@qui-cli/env 5.1.0 → 5.1.1

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [5.1.1](https://github.com/battis/qui-cli/compare/env/5.1.0...env/5.1.1) (2026-01-21)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * configure 1Password before looking up secret references ([7046af5](https://github.com/battis/qui-cli/commit/7046af55ed2720b7b74bd437f335fe49fa8bc00f))
11
+
5
12
  ## [5.1.0](https://github.com/battis/qui-cli/compare/env/5.0.4...env/5.1.0) (2026-01-19)
6
13
 
7
14
 
@@ -1,12 +1,36 @@
1
1
  import * as Plugin from '@qui-cli/plugin';
2
- import { OPConfiguration } from './Configuration.js';
3
- export declare const name = "1password support";
4
- export declare function configure(proposal?: OPConfiguration): Promise<void>;
5
- export declare function options(): Plugin.Options;
6
- export declare function init({ values }: Plugin.ExpectedArguments<typeof options>): Promise<void>;
7
- export declare function get(ref: string): Promise<string>;
8
- /** Requires a service account with write privileges */
9
- export declare function set({ ref, value }: {
10
- ref: string;
11
- value: string;
12
- }): Promise<void>;
2
+ import * as Base from './Base.js';
3
+ import * as Secrets from './Secrets.js';
4
+ export type Configuration = Plugin.Configuration & {
5
+ /**
6
+ * 1Password service account token; will use the environment variable OP_TOKEN
7
+ * if present
8
+ */
9
+ opToken?: string;
10
+ /**
11
+ * Name or ID of the 1Password API Credential item storing the 1Password
12
+ * service account token; will use environment variable OP_ITEM if present
13
+ */
14
+ opItem?: string;
15
+ /**
16
+ * 1Password account to use (if signed into multiple); will use environment
17
+ * variable OP_ACCOUNT if present
18
+ */
19
+ opAccount?: string;
20
+ };
21
+ export declare class OP implements Base.Plugin {
22
+ [key: string]: unknown;
23
+ readonly name = "env.1password";
24
+ private config;
25
+ private _client;
26
+ private getClient;
27
+ configure(proposal?: Configuration): void;
28
+ private configureFromEnv;
29
+ options(): Plugin.Options;
30
+ init({ values }: Plugin.ExpectedArguments<typeof this.options>): Promise<void>;
31
+ get({ ref, env }: Base.GetOptions): Promise<string>;
32
+ isSecretReference: typeof Secrets.isRef;
33
+ private itemFrom;
34
+ /** Requires a service account with write privileges */
35
+ set({ ref, value, env }: Base.SetOptions): Promise<void>;
36
+ }
@@ -5,153 +5,152 @@ import { Log } from '@qui-cli/log';
5
5
  import { Shell } from '@qui-cli/shell';
6
6
  import path from 'node:path';
7
7
  import ora from 'ora';
8
- export const name = '1password support';
9
- const config = {};
10
- let _client = undefined;
11
- async function getClient() {
12
- if (!_client) {
13
- const spinner = ora('Loading 1Password').start();
14
- if (config.opItem && !config.opToken) {
15
- const silent = Shell.isSilent();
16
- const showCommands = Shell.commandsShown();
17
- const logging = Shell.isLogging();
18
- Shell.configure({ silent: true, showCommands: false, logging: false });
19
- if (/(\d+\.)+\d/.test(Shell.exec('op -v').stdout)) {
20
- const { stdout, stderr } = Shell.exec(`op item get ${config.opAccount ? `--account "${config.opAccount}" ` : ''}--reveal --fields credential "${config.opItem}"`);
21
- if (stdout.length) {
22
- config.opToken = stdout.trim();
8
+ import * as Secrets from './Secrets.js';
9
+ export class OP {
10
+ name = 'env.1password';
11
+ config = {};
12
+ _client = undefined;
13
+ async getClient() {
14
+ if (!this._client) {
15
+ const spinner = ora('Loading 1Password').start();
16
+ if (this.config.opItem && !this.config.opToken) {
17
+ const silent = Shell.isSilent();
18
+ const showCommands = Shell.commandsShown();
19
+ const logging = Shell.isLogging();
20
+ Shell.configure({ silent: true, showCommands: false, logging: false });
21
+ if (/(\d+\.)+\d/.test(Shell.exec('op -v').stdout)) {
22
+ const { stdout, stderr } = Shell.exec(`op item get ${this.config.opAccount ? `--account "${this.config.opAccount}" ` : ''}--reveal --fields credential "${this.config.opItem}"`);
23
+ if (stdout.length) {
24
+ this.config.opToken = stdout.trim();
25
+ }
26
+ else {
27
+ Log.fatal(stderr);
28
+ process.exit(1);
29
+ }
23
30
  }
24
31
  else {
25
- Log.fatal(stderr);
26
- process.exit(1);
32
+ throw new Error(`Looking up a 1Password service account token by item identifier requires the 1Password CLI (${Colors.url('https://developer.1password.com/docs/cli')}).`);
27
33
  }
34
+ Shell.configure({ silent, showCommands, logging });
35
+ }
36
+ if (this.config.opToken) {
37
+ const pkg = await importLocal(path.join(import.meta.dirname, '../../package.json'));
38
+ this._client = await createClient({
39
+ auth: this.config.opToken,
40
+ integrationName: pkg
41
+ .name.replace(/^(\/|@)/, '')
42
+ .replace(/[/@]+/g, '-'),
43
+ integrationVersion: pkg.version
44
+ });
45
+ spinner.succeed('1Password loaded');
28
46
  }
29
47
  else {
30
- throw new Error(`Looking up a 1Password service account token by item identifier requires the 1Password CLI (${Colors.url('https://developer.1password.com/docs/cli')}).`);
48
+ spinner.fail();
49
+ throw new Error('A 1Password service account token was not provided.');
31
50
  }
32
- Shell.configure({ silent, showCommands, logging });
33
- }
34
- if (config.opToken) {
35
- const pkg = await importLocal(path.join(import.meta.dirname, '../../package.json'));
36
- _client = await createClient({
37
- auth: config.opToken,
38
- integrationName: pkg
39
- .name.replace(/^(\/|@)/, '')
40
- .replace(/[/@]+/g, '-'),
41
- integrationVersion: pkg.version
42
- });
43
- spinner.succeed('1Password loaded');
44
- }
45
- else {
46
- const message = 'A 1Password service account token was not provided.';
47
- spinner.fail(message);
48
- throw new Error('A 1Password service account token was not provided.');
49
51
  }
52
+ return this._client;
50
53
  }
51
- return _client;
52
- }
53
- export async function configure(proposal = {}) {
54
- for (const key in proposal) {
55
- if (proposal[key] !== undefined) {
56
- config[key] = proposal[key];
54
+ configure(proposal = {}) {
55
+ for (const key in proposal) {
56
+ if (proposal[key] !== undefined) {
57
+ this.config[key] = proposal[key];
58
+ }
57
59
  }
58
60
  }
59
- }
60
- export function options() {
61
- return {
62
- man: [
63
- {
64
- level: 1,
65
- text: '1Password environment integration'
66
- },
67
- {
68
- text: `If 1Password secret references are stored in the environment, a ` +
69
- `1Password service account token is required to access the secret ` +
70
- `values.`
71
- }
72
- ],
73
- opt: {
74
- opAccount: {
75
- description: `1Password account to use (if signed into multiple); will use ` +
76
- `environment variable ${Colors.varName('OP_ACCOUNT')} if present`,
77
- hint: 'example.1password.com',
78
- default: config.opAccount
79
- },
80
- opItem: {
81
- description: `Name or ID of the 1Password API Credential item storing the ` +
82
- `1Password service account token; will use environment variable ` +
83
- `${Colors.varName('OP_ITEM')} if present. Requires the 1Password ` +
84
- `CLI tool (${Colors.url('https://developer.1password.com/docs/cli')})`,
85
- hint: '1Password unique identifier',
86
- default: config.opItem
87
- },
88
- opToken: {
89
- description: `1Password service account token; will use environment variable ` +
90
- `${Colors.varName('OP_TOKEN')} if present`,
91
- hint: 'token value',
92
- secret: true,
93
- default: config.opToken
61
+ configureFromEnv(env) {
62
+ this.configure({
63
+ opAccount: env.OP_ACCOUNT,
64
+ opItem: env.OP_ITEM,
65
+ opToken: env.OP_TOKEN
66
+ });
67
+ }
68
+ options() {
69
+ return {
70
+ man: [
71
+ {
72
+ level: 1,
73
+ text: '1Password environment integration'
74
+ },
75
+ {
76
+ text: `If 1Password secret references are stored in the environment, a ` +
77
+ `1Password service account token is required to access the secret ` +
78
+ `values.`
79
+ }
80
+ ],
81
+ opt: {
82
+ opAccount: {
83
+ description: `1Password account to use (if signed into multiple); will use ` +
84
+ `environment variable ${Colors.varName('OP_ACCOUNT')} if present`,
85
+ hint: 'example.1password.com',
86
+ default: this.config.opAccount
87
+ },
88
+ opItem: {
89
+ description: `Name or ID of the 1Password API Credential item storing the ` +
90
+ `1Password service account token; will use environment variable ` +
91
+ `${Colors.varName('OP_ITEM')} if present. Requires the 1Password ` +
92
+ `CLI tool (${Colors.url('https://developer.1password.com/docs/cli')})`,
93
+ hint: '1Password unique identifier',
94
+ default: this.config.opItem
95
+ },
96
+ opToken: {
97
+ description: `1Password service account token; will use environment variable ` +
98
+ `${Colors.varName('OP_TOKEN')} if present`,
99
+ hint: 'token value',
100
+ secret: true,
101
+ default: this.config.opToken
102
+ }
94
103
  }
95
- }
96
- };
97
- }
98
- export async function init({ values }) {
99
- const { opAccount = process.env.OP_ACCOUNT, opItem = process.env.OP_ITEM, opToken = process.env.OP_TOKEN } = values;
100
- await configure({ ...values, opAccount, opItem, opToken });
101
- }
102
- export async function get(ref) {
103
- const client = await getClient();
104
- return await client.secrets.resolve(ref);
105
- }
106
- function explodeSecretReference(secretReference) {
107
- // eslint-disable-next-line prefer-const
108
- let [vault, item, section, field] = secretReference
109
- .replace(/^op:\/\//, '')
110
- .split('/');
111
- if (field === undefined) {
112
- field = section;
113
- section = '';
104
+ };
114
105
  }
115
- return { vault, item, section, field };
116
- }
117
- async function itemFrom(parts) {
118
- const client = await getClient();
119
- const vault = (await client.vaults.list())
120
- .filter((vault) => vault.title == parts.vault)
121
- .shift();
122
- if (vault) {
123
- const item = (await client.items.list(vault.id))
124
- .filter((item) => item.title === parts.item || item.id === parts.item)
106
+ async init({ values }) {
107
+ await this.configure(values);
108
+ }
109
+ async get({ ref, env }) {
110
+ this.configureFromEnv(env);
111
+ return await (await this.getClient()).secrets.resolve(ref);
112
+ }
113
+ isSecretReference = Secrets.isRef;
114
+ async itemFrom(parts) {
115
+ const client = await this.getClient();
116
+ const vault = (await client.vaults.list())
117
+ .filter((vault) => vault.title == parts.vault)
125
118
  .shift();
126
- if (item) {
127
- return await client.items.get(vault.id, item.id);
119
+ if (vault) {
120
+ const item = (await client.items.list(vault.id))
121
+ .filter((item) => item.title === parts.item || item.id === parts.item)
122
+ .shift();
123
+ if (item) {
124
+ return await client.items.get(vault.id, item.id);
125
+ }
128
126
  }
129
127
  }
130
- }
131
- /** Requires a service account with write privileges */
132
- export async function set({ ref, value }) {
133
- const client = await getClient();
134
- const parts = explodeSecretReference(ref);
135
- const item = await itemFrom(parts);
136
- if (item) {
137
- const updated = {
138
- ...item,
139
- fields: item.fields.map((field) => {
140
- const section = item.sections.find((s) => s.id === field.sectionId);
141
- if (parts.field === field.title || parts.field === field.id) {
142
- if ((!parts.section &&
143
- (field.sectionId === '' || field.sectionId === 'add more')) ||
144
- parts.section === section?.title ||
145
- parts.section === section?.id) {
146
- return { ...field, value };
128
+ /** Requires a service account with write privileges */
129
+ async set({ ref, value, env }) {
130
+ this.configureFromEnv(env);
131
+ const client = await this.getClient();
132
+ const parts = Secrets.explodeRef(ref);
133
+ const item = await this.itemFrom(parts);
134
+ if (item) {
135
+ const updated = {
136
+ ...item,
137
+ fields: item.fields.map((field) => {
138
+ const section = item.sections.find((s) => s.id === field.sectionId);
139
+ if (parts.field === field.title || parts.field === field.id) {
140
+ if ((!parts.section &&
141
+ (field.sectionId === '' || field.sectionId === 'add more')) ||
142
+ parts.section === section?.title ||
143
+ parts.section === section?.id) {
144
+ return { ...field, value };
145
+ }
147
146
  }
148
- }
149
- return field;
150
- })
151
- };
152
- await client.items.put(updated);
153
- }
154
- else {
155
- throw new Error('1Password item could not be found');
147
+ return field;
148
+ })
149
+ };
150
+ await client.items.put(updated);
151
+ }
152
+ else {
153
+ throw new Error('1Password item could not be found');
154
+ }
156
155
  }
157
156
  }
@@ -0,0 +1,18 @@
1
+ import { Base } from '@qui-cli/plugin/dist/Plugin.js';
2
+ import { isRef } from './Secrets.js';
3
+ type Environment = Record<string, string>;
4
+ export type GetOptions = {
5
+ ref: string;
6
+ env: Environment;
7
+ };
8
+ export type SetOptions = {
9
+ ref: string;
10
+ value: string;
11
+ env: Environment;
12
+ };
13
+ export interface Plugin extends Base {
14
+ isSecretReference: typeof isRef;
15
+ get(options: GetOptions): Promise<string | undefined>;
16
+ set(options: SetOptions): Promise<void>;
17
+ }
18
+ export {};
@@ -0,0 +1,8 @@
1
+ export type Reference = {
2
+ vault: string;
3
+ item: string;
4
+ section?: string;
5
+ field: string;
6
+ };
7
+ export declare function isRef(value: unknown): boolean;
8
+ export declare function explodeRef(ref: string): Reference;
@@ -0,0 +1,15 @@
1
+ export function isRef(value) {
2
+ return (!!value &&
3
+ value !== null &&
4
+ typeof value === 'string' &&
5
+ /^op:\/\//.test(value));
6
+ }
7
+ export function explodeRef(ref) {
8
+ // eslint-disable-next-line prefer-const
9
+ let [vault, item, section, field] = ref.replace(/^op:\/\//, '').split('/');
10
+ if (field === undefined) {
11
+ field = section;
12
+ section = '';
13
+ }
14
+ return { vault, item, section, field };
15
+ }
@@ -1,3 +1,6 @@
1
- export * from './1Password.js';
2
- export * from './Configuration.js';
3
- export * from './isSecretReference.js';
1
+ import { Plugin } from './Base.js';
2
+ export declare const OP: {
3
+ isSecretReference: (value: unknown) => boolean;
4
+ get?: Plugin['get'];
5
+ set?: Plugin['set'];
6
+ };
@@ -1,3 +1,17 @@
1
- export * from './1Password.js';
2
- export * from './Configuration.js';
3
- export * from './isSecretReference.js';
1
+ import { register } from '@qui-cli/plugin';
2
+ import { isRef } from './Secrets.js';
3
+ export const OP = {
4
+ isSecretReference: isRef
5
+ };
6
+ try {
7
+ await import('@1password/sdk');
8
+ const opModule = await import('./1Password.js');
9
+ const plugin = new opModule.OP();
10
+ OP.set = plugin.set.bind(plugin);
11
+ OP.get = plugin.get.bind(plugin);
12
+ await register(plugin);
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ }
15
+ catch (_) {
16
+ //
17
+ }
package/dist/Env.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as Plugin from '@qui-cli/plugin';
2
2
  import dotenv from 'dotenv';
3
- import { OPConfiguration } from './1Password/Configuration.js';
4
3
  export type Configuration = Plugin.Configuration & {
5
4
  /**
6
5
  * Optional root for calculating relative paths to `.env` files. If undefined,
@@ -15,11 +14,10 @@ export type Configuration = Plugin.Configuration & {
15
14
  load?: boolean;
16
15
  /** Path to desired `.env` file relative to `root`. Defaults to `'.env'`; */
17
16
  path?: string;
18
- } & OPConfiguration;
17
+ };
19
18
  export declare const name = "env";
20
19
  export declare function configure(proposal?: Configuration): Promise<void>;
21
- export declare function options(): Promise<Plugin.Options>;
22
- export declare function init(args: Plugin.ExpectedArguments<typeof options>): Promise<void>;
20
+ export declare function init(): Promise<void>;
23
21
  export type ParsedResult = dotenv.DotenvParseOutput;
24
22
  export declare function parse(file?: string | undefined): Promise<ParsedResult>;
25
23
  export type GetOptions = {
package/dist/Env.js CHANGED
@@ -2,16 +2,7 @@ import { Root } from '@qui-cli/root';
2
2
  import dotenv from 'dotenv';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { isSecretReference } from './1Password/isSecretReference.js';
6
- let OP = undefined;
7
- try {
8
- await import('@1password/sdk');
9
- OP = await import('./1Password/index.js');
10
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
- }
12
- catch (_) {
13
- // @1password/sdk not present
14
- }
5
+ import { OP } from './1Password/index.js';
15
6
  export const name = 'env';
16
7
  const config = {
17
8
  load: true,
@@ -23,30 +14,11 @@ export async function configure(proposal = {}) {
23
14
  config[key] = proposal[key];
24
15
  }
25
16
  }
26
- if (OP?.configure) {
27
- OP.configure({
28
- opAccount: config.opAccount,
29
- opItem: config.opItem,
30
- opToken: config.opToken
31
- });
32
- }
33
17
  if (config.load) {
34
18
  await parse();
35
19
  }
36
20
  }
37
- export async function options() {
38
- if (OP?.options) {
39
- return await OP.options();
40
- }
41
- else {
42
- return {};
43
- }
44
- }
45
- export async function init(args) {
46
- if (OP?.init) {
47
- await OP.init(args);
48
- }
49
- await configure(args.values);
21
+ export async function init() {
50
22
  parse();
51
23
  }
52
24
  function toFilePath(file = '.env') {
@@ -66,9 +38,9 @@ export async function parse(file = config.path) {
66
38
  export async function get({ key, file = config.path || '.env' }) {
67
39
  if (fs.existsSync(toFilePath(file))) {
68
40
  const env = await parse(file);
69
- if (isSecretReference(env[key])) {
70
- if (OP?.get) {
71
- return OP?.get(env[key]);
41
+ if (OP.isSecretReference(env[key])) {
42
+ if (OP.get) {
43
+ return await OP.get({ ref: env[key], env });
72
44
  }
73
45
  else {
74
46
  throw new Error(`Attempt to read environment variable ${key} that is a 1Password secret reference without @1password/sdk installed.`);
@@ -86,11 +58,12 @@ export async function exists({ key, file = config.path }) {
86
58
  }
87
59
  export async function set({ key, value, file = config.path, comment, ifNotExists = false }) {
88
60
  const filePath = toFilePath(file);
89
- const { [key]: prev } = dotenv.config({ path: filePath, quiet: true }).parsed || {};
61
+ const env = dotenv.config({ path: filePath, quiet: true }).parsed || {};
62
+ const { [key]: prev } = env;
90
63
  if (ifNotExists === false || !prev) {
91
- if (prev && isSecretReference(prev)) {
92
- if (OP?.set) {
93
- OP.set({ ref: prev, value });
64
+ if (prev && OP.isSecretReference(prev)) {
65
+ if (OP.set) {
66
+ await OP.set({ ref: prev, value, env });
94
67
  }
95
68
  else {
96
69
  throw new Error(`Attmept to update environment variable ${key} that is a 1Password secret reference without installing @1password/sdk`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qui-cli/env",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "@qui-cli Plugin: Standardized environment configuration",
5
5
  "homepage": "https://github.com/battis/qui-cli/tree/main/packages/env#readme",
6
6
  "repository": {
@@ -1,18 +0,0 @@
1
- import * as Plugin from '@qui-cli/plugin';
2
- export type OPConfiguration = Plugin.Configuration & {
3
- /**
4
- * 1Password service account token; will use the environment variable OP_TOKEN
5
- * if present
6
- */
7
- opToken?: string;
8
- /**
9
- * Name or ID of the 1Password API Credential item storing the 1Password
10
- * service account token; will use environment variable OP_ITEM if present
11
- */
12
- opItem?: string;
13
- /**
14
- * 1Password account to use (if signed into multiple); will use environment
15
- * variable OP_ACCOUNT if present
16
- */
17
- opAccount?: string;
18
- };
@@ -1 +0,0 @@
1
- export declare function isSecretReference(value: unknown): unknown;
@@ -1,6 +0,0 @@
1
- export function isSecretReference(value) {
2
- return (value &&
3
- value !== null &&
4
- typeof value === 'string' &&
5
- /^op:\/\//.test(value));
6
- }
File without changes