@quonfig/cli 0.0.45 → 0.0.47

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.
@@ -42,6 +42,15 @@ export declare const spawnGit: (args: string[], options?: SpawnGitOptions) => Pr
42
42
  export declare const gitClone: (repoUrl: string, dir: string) => Promise<void>;
43
43
  export declare const isGitRepo: (dir: string) => Promise<boolean>;
44
44
  export declare const getRemoteUrl: (dir: string) => Promise<string | null>;
45
+ /**
46
+ * Return every configured remote URL (one per remote name). Walks the
47
+ * output of `git remote` and resolves each name's URL via `remote get-url`.
48
+ *
49
+ * Returns an empty array when the dir isn't a git repo or has no remotes.
50
+ * Multi-remote support (qfg-glrd.3): the identity check accepts as long as
51
+ * any configured remote points at the backend's repo URL.
52
+ */
53
+ export declare const getAllRemoteUrls: (dir: string) => Promise<string[]>;
45
54
  export declare const isWorkingTreeClean: (dir: string) => Promise<boolean>;
46
55
  /**
47
56
  * Returns true if `file` (relative to `dir`) has any working-tree or
@@ -81,6 +81,44 @@ export const getRemoteUrl = async (dir) => {
81
81
  return null;
82
82
  }
83
83
  };
84
+ /**
85
+ * Return every configured remote URL (one per remote name). Walks the
86
+ * output of `git remote` and resolves each name's URL via `remote get-url`.
87
+ *
88
+ * Returns an empty array when the dir isn't a git repo or has no remotes.
89
+ * Multi-remote support (qfg-glrd.3): the identity check accepts as long as
90
+ * any configured remote points at the backend's repo URL.
91
+ */
92
+ export const getAllRemoteUrls = async (dir) => {
93
+ let names;
94
+ try {
95
+ const { stdout } = await runGit(['-C', dir, 'remote']);
96
+ names = stdout
97
+ .split('\n')
98
+ .map((s) => s.trim())
99
+ .filter(Boolean);
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ const urls = [];
105
+ for (const name of names) {
106
+ try {
107
+ // Sequential by design: each `git remote get-url` spawns its own git
108
+ // process against the same repo; running them serially keeps git's
109
+ // index/lock access predictable and the remote count is tiny.
110
+ // eslint-disable-next-line no-await-in-loop
111
+ const { stdout } = await runGit(['-C', dir, 'remote', 'get-url', name]);
112
+ const url = stdout.trim();
113
+ if (url.length > 0)
114
+ urls.push(url);
115
+ }
116
+ catch {
117
+ /* skip remotes whose URL we can't read */
118
+ }
119
+ }
120
+ return urls;
121
+ };
84
122
  export const isWorkingTreeClean = async (dir) => {
85
123
  const { stdout } = await runGit(['-C', dir, 'status', '--porcelain']);
86
124
  return stdout.trim() === '';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Resolve the workspace directory for `qfg push` / `qfg pull`.
3
+ *
4
+ * Resolution order:
5
+ * 1. `--dir <path>` flag (explicit, wins everything)
6
+ * 2. `QUONFIG_DIR` env var
7
+ * 3. Walk up from cwd looking for `quonfig.json`. Stop at the first hit,
8
+ * the user's home dir, or the filesystem root.
9
+ * 4. Error: `No Quonfig workspace dir found. ...`
10
+ *
11
+ * Pure — the caller wires `process.cwd()` and `process.env.QUONFIG_DIR` in.
12
+ * Tests drive the function with explicit values so the cwd walk can be
13
+ * exercised inside a tmp dir without `process.chdir`.
14
+ *
15
+ * Empty-string flag / env values are treated as "absent" because shells
16
+ * routinely emit `EXPORT_FOO=` when a var is declared but has no value;
17
+ * silently picking the empty string would resolve to cwd and produce a
18
+ * confusing wrong-target failure downstream.
19
+ */
20
+ export declare const NO_WORKSPACE_DIR_ERROR = "No Quonfig workspace dir found. Run from inside a workspace, or pass --dir, or set QUONFIG_DIR.";
21
+ export interface ResolveWorkspaceDirInput {
22
+ /** Current working directory — the search root for the walk. */
23
+ cwd: string;
24
+ /** Value of `QUONFIG_DIR` env var (or undefined). */
25
+ envDir: string | undefined;
26
+ /** Value of `--dir` flag (or undefined). */
27
+ flagDir: string | undefined;
28
+ /**
29
+ * The user's home directory. The cwd walk stops here (without checking
30
+ * it) so we never pick up a stray `quonfig.json` parked in `~`. Defaults
31
+ * to `os.homedir()` when omitted; tests pass an explicit value so they
32
+ * can pretend a tmp dir is "home".
33
+ */
34
+ homeDir?: string;
35
+ }
36
+ export type ResolveWorkspaceDirResult = {
37
+ kind: 'ok';
38
+ dir: string;
39
+ source: 'flag' | 'env' | 'cwd-walk';
40
+ } | {
41
+ kind: 'error';
42
+ message: string;
43
+ };
44
+ export declare const resolveWorkspaceDir: (input: ResolveWorkspaceDirInput) => ResolveWorkspaceDirResult;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resolve the workspace directory for `qfg push` / `qfg pull`.
3
+ *
4
+ * Resolution order:
5
+ * 1. `--dir <path>` flag (explicit, wins everything)
6
+ * 2. `QUONFIG_DIR` env var
7
+ * 3. Walk up from cwd looking for `quonfig.json`. Stop at the first hit,
8
+ * the user's home dir, or the filesystem root.
9
+ * 4. Error: `No Quonfig workspace dir found. ...`
10
+ *
11
+ * Pure — the caller wires `process.cwd()` and `process.env.QUONFIG_DIR` in.
12
+ * Tests drive the function with explicit values so the cwd walk can be
13
+ * exercised inside a tmp dir without `process.chdir`.
14
+ *
15
+ * Empty-string flag / env values are treated as "absent" because shells
16
+ * routinely emit `EXPORT_FOO=` when a var is declared but has no value;
17
+ * silently picking the empty string would resolve to cwd and produce a
18
+ * confusing wrong-target failure downstream.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import * as path from 'node:path';
23
+ const QUONFIG_JSON = 'quonfig.json';
24
+ export const NO_WORKSPACE_DIR_ERROR = 'No Quonfig workspace dir found. Run from inside a workspace, or pass --dir, or set QUONFIG_DIR.';
25
+ const blank = (s) => s === undefined || s === '';
26
+ export const resolveWorkspaceDir = (input) => {
27
+ if (!blank(input.flagDir)) {
28
+ return { kind: 'ok', dir: path.resolve(input.cwd, input.flagDir), source: 'flag' };
29
+ }
30
+ if (!blank(input.envDir)) {
31
+ return { kind: 'ok', dir: path.resolve(input.cwd, input.envDir), source: 'env' };
32
+ }
33
+ const homeDir = input.homeDir ?? os.homedir();
34
+ const found = walkUpForWorkspace(input.cwd, homeDir);
35
+ if (found !== undefined) {
36
+ return { kind: 'ok', dir: found, source: 'cwd-walk' };
37
+ }
38
+ return { kind: 'error', message: NO_WORKSPACE_DIR_ERROR };
39
+ };
40
+ /**
41
+ * Walk from `start` toward the filesystem root, returning the first ancestor
42
+ * dir that contains a `quonfig.json`. Stops without checking when the walk
43
+ * reaches `homeDir` or the filesystem root — `~` is not a workspace.
44
+ */
45
+ const walkUpForWorkspace = (start, homeDir) => {
46
+ let current = path.resolve(start);
47
+ // Normalize homeDir the same way so the equality check survives a
48
+ // trailing slash or symlink-resolved path on the caller's side.
49
+ const stop = path.resolve(homeDir);
50
+ // Walk up until the filesystem root: path.dirname() of the root is the
51
+ // root itself, so `parent === current` is the terminal condition.
52
+ for (let parent = path.dirname(current);; current = parent, parent = path.dirname(current)) {
53
+ if (current === stop)
54
+ return undefined;
55
+ if (fs.existsSync(path.join(current, QUONFIG_JSON))) {
56
+ return current;
57
+ }
58
+ if (parent === current)
59
+ return undefined;
60
+ }
61
+ };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "0.0.45";
1
+ declare const _default: "0.0.47";
2
2
  export default _default;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
2
- export default '0.0.45';
2
+ export default '0.0.47';
@@ -1383,6 +1383,12 @@
1383
1383
  ],
1384
1384
  "type": "option"
1385
1385
  },
1386
+ "full-summary": {
1387
+ "description": "Reify the full source-side audit log into per-change git commits (author = original user, date = original timestamp, message = change summary). Only valid on first-run imports — pass --reset to re-import everything from scratch with this flag.",
1388
+ "name": "full-summary",
1389
+ "allowNo": false,
1390
+ "type": "boolean"
1391
+ },
1386
1392
  "push": {
1387
1393
  "description": "After migrating to a local dir, also push to the given --workspace on Quonfig cloud",
1388
1394
  "name": "push",
@@ -1637,7 +1643,7 @@
1637
1643
  "type": "boolean"
1638
1644
  },
1639
1645
  "dir": {
1640
- "description": "Local directory to clone/update (defaults to QUONFIG_DIR env var)",
1646
+ "description": "Local directory to clone/update (defaults to cwd / nearest ancestor with quonfig.json / QUONFIG_DIR env var)",
1641
1647
  "name": "dir",
1642
1648
  "hasDynamicHelp": false,
1643
1649
  "multiple": false,
@@ -1675,7 +1681,7 @@
1675
1681
  "push": {
1676
1682
  "aliases": [],
1677
1683
  "args": {},
1678
- "description": "Push local config changes up to your workspace on Quonfig cloud.\n\nEnforces three guards before touching the remote:\n 1. The dir's quonfig.json \"workspace\" pin must match the backend (if set).\n 2. The dir's git origin must match the backend repo URL (if set).\n 3. A diff summary is shown; destructive changes (10+ deletes, >=25% of\n files, or an unpinned dir) require typing the workspace slug to confirm.\n\n qfg push # uses QUONFIG_DIR + profile workspace\n qfg push --dir ./our-config\n qfg push --dir ./our-config --workspace acme-prod\n qfg push --dir ./our-config --yes # skip normal Y/N (never skips typed-slug)\n qfg push --dir ./our-config --skip-validate",
1684
+ "description": "Push local config changes up to your workspace on Quonfig cloud.\n\nEnforces three guards before touching the remote:\n 1. The dir's quonfig.json \"workspace\" pin must match the backend (if set).\n 2. The dir's git origin must match the backend repo URL (if set).\n 3. A diff summary is shown; destructive changes (10+ deletes, >=25% of\n files, or an unpinned dir) require typing the workspace slug to confirm.\n\n qfg push # resolves dir from cwd, QUONFIG_DIR, or --dir\n qfg push --dir ./our-config\n qfg push --dir ./our-config --workspace acme-prod\n qfg push --dir ./our-config --yes # skip normal Y/N (never skips typed-slug)\n qfg push --dir ./our-config --skip-validate",
1679
1685
  "examples": [
1680
1686
  "<%= config.bin %> push --dir ./our-config",
1681
1687
  "<%= config.bin %> push --workspace acme-prod --dir ./our-config",
@@ -1712,7 +1718,7 @@
1712
1718
  "type": "boolean"
1713
1719
  },
1714
1720
  "dir": {
1715
- "description": "Local directory to push (defaults to QUONFIG_DIR env var)",
1721
+ "description": "Local directory to push (defaults to cwd / nearest ancestor with quonfig.json / QUONFIG_DIR env var)",
1716
1722
  "name": "dir",
1717
1723
  "hasDynamicHelp": false,
1718
1724
  "multiple": false,
@@ -3602,5 +3608,5 @@
3602
3608
  ]
3603
3609
  }
3604
3610
  },
3605
- "version": "0.0.45"
3611
+ "version": "0.0.47"
3606
3612
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
3
3
  "name": "@quonfig/cli",
4
- "version": "0.0.45",
4
+ "version": "0.0.47",
5
5
  "bugs": {
6
6
  "url": "https://github.com/quonfig/cli/issues"
7
7
  },
@@ -23,7 +23,7 @@
23
23
  "@oclif/plugin-plugins": "^4",
24
24
  "@quonfig/node": "^0.0.24",
25
25
  "acorn": "^8.10.0",
26
- "acorn-walk": "^8.2.0",
26
+ "acorn-walk": "^8.3.5",
27
27
  "chalk": "^5.4.1",
28
28
  "common-tags": "^1.8.2",
29
29
  "fuzzy": "^0.1.3",
@@ -32,7 +32,7 @@
32
32
  "mustache": "^4.2.0",
33
33
  "node-forge": "^1.4.0",
34
34
  "typescript": "^5",
35
- "zod": "^4.1.12"
35
+ "zod": "^4.4.3"
36
36
  },
37
37
  "description": "Quonfig CLI.\n\n Inject configs/secrets as env vars (preferred for scripts and CI):\n qfg run --env DATABASE_URL=db.url --env API_KEY=stripe.key -- ./bin/migrate\n\n Other common usage:\n qfg get <key> --environment <env> read a single value\n qfg list show all keys in the workspace\n qfg config-schema && qfg pull set up arbitrary targeting rules (email, plan, segment, custom property)",
38
38
  "devDependencies": {
@@ -41,7 +41,7 @@
41
41
  "@types/chai": "^4",
42
42
  "@types/common-tags": "^1.8.4",
43
43
  "@types/lodash.camelcase": "^4.3.9",
44
- "@types/mocha": "^10",
44
+ "@types/mocha": "^10.0.10",
45
45
  "@types/mustache": "^4.2.5",
46
46
  "@types/node": "^18",
47
47
  "@types/node-forge": "^1.3.14",
@@ -52,10 +52,10 @@
52
52
  "eslint-config-prettier": "^9.0.0",
53
53
  "eslint-plugin-markdown": "^5.1.0",
54
54
  "eslint-plugin-only-error": "^1.0.2",
55
- "mocha": "^10",
56
- "msw": "^2.0.6",
55
+ "mocha": "^11.7.5",
56
+ "msw": "^2.14.6",
57
57
  "oclif": "^4.22.29",
58
- "prettier": "^3.6.2",
58
+ "prettier": "^3.8.3",
59
59
  "shx": "^0.3.4",
60
60
  "ts-node": "^10.9.1",
61
61
  "tsx": "^4.21.0"