@quonfig/cli 0.0.25 → 0.0.26
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/README.md +47 -47
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.js +33 -0
- package/dist/commands/delete.js +4 -2
- package/dist/commands/init.js +6 -2
- package/dist/commands/login.js +72 -91
- package/dist/commands/migrate/doctor.js +13 -3
- package/dist/commands/override.js +4 -2
- package/dist/commands/pull.d.ts +11 -0
- package/dist/commands/pull.js +67 -20
- package/dist/commands/push.js +4 -7
- package/dist/commands/whoami.d.ts +13 -0
- package/dist/commands/whoami.js +119 -32
- package/dist/commands/workspace/bootstrap.js +27 -20
- package/dist/commands/workspace/create.d.ts +11 -0
- package/dist/commands/workspace/create.js +56 -16
- package/dist/commands/workspace/switch.d.ts +10 -0
- package/dist/commands/workspace/switch.js +98 -50
- package/dist/commands/workspace.d.ts +14 -0
- package/dist/commands/workspace.js +152 -40
- package/dist/migrate/local-write.js +16 -31
- package/dist/push/bare-path-diff.js +2 -4
- package/dist/push/config-push-client.js +4 -2
- package/dist/push/run-push.js +37 -8
- package/dist/util/clone-and-stack-push.js +14 -29
- package/dist/util/get-client.js +70 -38
- package/dist/util/get-valid-token.d.ts +16 -4
- package/dist/util/get-valid-token.js +59 -42
- package/dist/util/git-ops.d.ts +90 -0
- package/dist/util/git-ops.js +172 -24
- package/dist/util/gitea-api.js +4 -2
- package/dist/util/local-config-writer.d.ts +33 -0
- package/dist/util/local-config-writer.js +63 -0
- package/dist/util/login-orchestrator.d.ts +42 -0
- package/dist/util/login-orchestrator.js +79 -0
- package/dist/util/oauth-client.d.ts +2 -0
- package/dist/util/oauth-client.js +30 -0
- package/dist/util/quonfig-json.d.ts +58 -21
- package/dist/util/quonfig-json.js +151 -22
- package/dist/util/resolve-workspace.js +79 -17
- package/dist/util/token-storage.d.ts +39 -8
- package/dist/util/token-storage.js +34 -4
- package/oclif.manifest.json +8 -8
- package/package.json +1 -1
package/dist/util/get-client.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Client } from '@quonfig/node';
|
|
2
2
|
import { getApiUrl } from '../util/domain-urls.js';
|
|
3
3
|
import jsonMaybe from '../util/json-maybe.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { tryParseWorkspacePin } from '../util/quonfig-json.js';
|
|
5
|
+
import { findOrgIdBySlug, getActiveProfile, loadAuthConfig, loadTokens } from '../util/token-storage.js';
|
|
6
|
+
import { getValidAccessToken, resolveDefaultOrgId } from '../util/get-valid-token.js';
|
|
6
7
|
import version from '../version.js';
|
|
8
|
+
const BARE_SLUG_ENV_MIGRATION_MESSAGE = 'QUONFIG_WORKSPACE must be in org/workspace form (e.g. acme/foo). ' +
|
|
9
|
+
'Bare workspace slugs are no longer accepted. Update your .env and run `qfg login` if you have not yet migrated.';
|
|
7
10
|
let clientInstance;
|
|
8
11
|
let cachedWorkspaceId;
|
|
9
12
|
export const resetClientCache = () => {
|
|
@@ -27,9 +30,9 @@ const getClient = async (command, sdkKey, profile) => {
|
|
|
27
30
|
}
|
|
28
31
|
else if (apiKey && apiKey.length > 0) {
|
|
29
32
|
// API-key (workspace-scoped bearer) path. getValidAccessToken short-circuits
|
|
30
|
-
// when QUONFIG_API_KEY is set, so this never touches disk.
|
|
33
|
+
// when QUONFIG_API_KEY is set, so this never touches disk and orgId is ignored.
|
|
31
34
|
try {
|
|
32
|
-
jwt = await getValidAccessToken(command.verboseLog);
|
|
35
|
+
jwt = await getValidAccessToken('', command.verboseLog);
|
|
33
36
|
}
|
|
34
37
|
catch (error) {
|
|
35
38
|
command.error(error instanceof Error ? error.message : String(error), { exit: 1 });
|
|
@@ -70,46 +73,75 @@ const getClient = async (command, sdkKey, profile) => {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
hasAuthConfig: Boolean(authConfig),
|
|
77
|
-
hasAccessToken: Boolean(tokens?.accessToken),
|
|
78
|
-
});
|
|
79
|
-
if (!authConfig || !tokens?.accessToken) {
|
|
80
|
-
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
81
|
-
}
|
|
82
|
-
command.verboseLog('Checking token validity...');
|
|
83
|
-
try {
|
|
84
|
-
jwt = await getValidAccessToken(command.verboseLog);
|
|
85
|
-
command.verboseLog('Token valid (or refreshed)');
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
89
|
-
command.error(`Session expired. Please run \`qfg login\` to re-authenticate. (${detail})`, { exit: 401 });
|
|
90
|
-
}
|
|
91
|
-
// Resolve workspace: --workspace/QUONFIG_WORKSPACE (slug) → QUONFIG_PROFILE (profile name) → default
|
|
76
|
+
// OAuth path: --workspace/QUONFIG_WORKSPACE must be in `org/ws` form so we
|
|
77
|
+
// can mint the org-scoped JWT for the right org and resolve the workspace
|
|
78
|
+
// slug *within that org*. No override → use the saved default profile.
|
|
92
79
|
const workspaceSlugOverride = profile ?? process.env.QUONFIG_WORKSPACE;
|
|
93
80
|
const profileNameOverride = process.env.QUONFIG_PROFILE;
|
|
81
|
+
const store = await loadTokens();
|
|
82
|
+
if (!store || Object.keys(store.tokensByOrg).length === 0) {
|
|
83
|
+
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
84
|
+
}
|
|
94
85
|
if (workspaceSlugOverride) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (profileData) {
|
|
104
|
-
workspaceId = profileData.workspace;
|
|
105
|
-
command.verboseLog('Workspace lookup', { source: 'profile-name', profile: workspaceSlugOverride, workspaceId });
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
command.error(`Workspace '${workspaceSlugOverride}' not found. Run \`qfg workspace switch\` to select and save a workspace.`, { exit: 1 });
|
|
109
|
-
}
|
|
86
|
+
const pin = tryParseWorkspacePin(workspaceSlugOverride);
|
|
87
|
+
if (!pin) {
|
|
88
|
+
command.error(BARE_SLUG_ENV_MIGRATION_MESSAGE, { exit: 1 });
|
|
89
|
+
}
|
|
90
|
+
const { orgSlug, workspaceSlug } = pin;
|
|
91
|
+
const orgId = findOrgIdBySlug(store, orgSlug);
|
|
92
|
+
if (!orgId) {
|
|
93
|
+
command.error(`No token found for org \`${orgSlug}\`. Run \`qfg login\` to add this org.`, { exit: 401 });
|
|
110
94
|
}
|
|
95
|
+
try {
|
|
96
|
+
jwt = await getValidAccessToken(orgId, command.verboseLog);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
100
|
+
command.error(`Session expired for org ${orgSlug}: ${detail}. Run \`qfg login\` to re-authenticate.`, {
|
|
101
|
+
exit: 401,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
let res;
|
|
105
|
+
try {
|
|
106
|
+
res = await fetch(`${getApiUrl()}/api/v1/userWorkspaces/list`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ json: {} }),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
command.error(`Failed to resolve workspace "${workspaceSlugOverride}": ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
114
|
+
}
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
command.error(`Failed to resolve workspace "${workspaceSlugOverride}" (HTTP ${res.status}).`, { exit: 1 });
|
|
117
|
+
}
|
|
118
|
+
const body = (await res.json());
|
|
119
|
+
const entries = (body.json ?? body);
|
|
120
|
+
const candidates = Array.isArray(entries) ? entries : [];
|
|
121
|
+
const match = candidates.find((w) => w.workosOrgId === orgId && w.workspaceSlug === workspaceSlug);
|
|
122
|
+
if (!match) {
|
|
123
|
+
const inOrg = candidates.filter((w) => w.workosOrgId === orgId).map((w) => w.workspaceSlug);
|
|
124
|
+
const available = inOrg.length > 0 ? inOrg.join(', ') : '(none)';
|
|
125
|
+
command.error(`Workspace "${workspaceSlug}" not found in org "${orgSlug}". Available: ${available}.`, { exit: 1 });
|
|
126
|
+
}
|
|
127
|
+
workspaceId = match.workspaceId;
|
|
128
|
+
command.verboseLog('Workspace lookup', { source: 'org/ws', orgSlug, workspaceSlug, orgId, workspaceId });
|
|
111
129
|
}
|
|
112
130
|
else {
|
|
131
|
+
const authConfig = await loadAuthConfig();
|
|
132
|
+
if (!authConfig) {
|
|
133
|
+
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
134
|
+
}
|
|
135
|
+
command.verboseLog('OAuth auth', { hasAuthConfig: true });
|
|
136
|
+
try {
|
|
137
|
+
const orgId = await resolveDefaultOrgId();
|
|
138
|
+
jwt = await getValidAccessToken(orgId, command.verboseLog);
|
|
139
|
+
command.verboseLog('Token valid (or refreshed)');
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
143
|
+
command.error(`Session expired. Please run \`qfg login\` to re-authenticate. (${detail})`, { exit: 401 });
|
|
144
|
+
}
|
|
113
145
|
const activeProfile = getActiveProfile(profileNameOverride);
|
|
114
146
|
const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
115
147
|
command.verboseLog('Workspace lookup', {
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns a valid (non-expired) WorkOS access token
|
|
3
|
-
* Throws if
|
|
2
|
+
* Returns a valid (non-expired) WorkOS access token for the given org,
|
|
3
|
+
* refreshing if needed. Throws if no token for that org is on disk or if
|
|
4
|
+
* refresh fails.
|
|
4
5
|
*
|
|
5
6
|
* If `QUONFIG_API_KEY` is set in the environment (non-empty), it is used
|
|
6
7
|
* directly as a bearer token and no disk reads/writes or refresh calls
|
|
7
|
-
* are made. This is the non-interactive (CI) path
|
|
8
|
+
* are made. This is the non-interactive (CI) path; `workosOrgId` is
|
|
9
|
+
* ignored because API keys are workspace-scoped.
|
|
8
10
|
*/
|
|
9
11
|
type Logger = (msg: string, data?: unknown) => void;
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Transitional helper: pick a default workosOrgId from the token store.
|
|
14
|
+
* Returns '' if no store exists (fine for the QUONFIG_API_KEY short-circuit
|
|
15
|
+
* path, which ignores the orgId argument).
|
|
16
|
+
*
|
|
17
|
+
* TODO(qfg-kr7.5/7/8/9): callers should pass through the workosOrgId resolved
|
|
18
|
+
* from the workspace address (org-slug/workspace-slug parsing). This helper
|
|
19
|
+
* is the stopgap until each caller's resolution lands in its own bead.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveDefaultOrgId(): Promise<string>;
|
|
22
|
+
export declare function getValidAccessToken(workosOrgId: string, log?: Logger): Promise<string>;
|
|
11
23
|
export {};
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns a valid (non-expired) WorkOS access token
|
|
3
|
-
* Throws if
|
|
2
|
+
* Returns a valid (non-expired) WorkOS access token for the given org,
|
|
3
|
+
* refreshing if needed. Throws if no token for that org is on disk or if
|
|
4
|
+
* refresh fails.
|
|
4
5
|
*
|
|
5
6
|
* If `QUONFIG_API_KEY` is set in the environment (non-empty), it is used
|
|
6
7
|
* directly as a bearer token and no disk reads/writes or refresh calls
|
|
7
|
-
* are made. This is the non-interactive (CI) path
|
|
8
|
+
* are made. This is the non-interactive (CI) path; `workosOrgId` is
|
|
9
|
+
* ignored because API keys are workspace-scoped.
|
|
8
10
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { loadTokens, saveTokens } from './token-storage.js';
|
|
11
|
+
import { authenticateWithOrg, decodeJWT } from './oauth-client.js';
|
|
12
|
+
import { getTokenForOrg, loadTokens, saveTokens } from './token-storage.js';
|
|
12
13
|
const noopLog = () => { };
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Transitional helper: pick a default workosOrgId from the token store.
|
|
16
|
+
* Returns '' if no store exists (fine for the QUONFIG_API_KEY short-circuit
|
|
17
|
+
* path, which ignores the orgId argument).
|
|
18
|
+
*
|
|
19
|
+
* TODO(qfg-kr7.5/7/8/9): callers should pass through the workosOrgId resolved
|
|
20
|
+
* from the workspace address (org-slug/workspace-slug parsing). This helper
|
|
21
|
+
* is the stopgap until each caller's resolution lands in its own bead.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveDefaultOrgId() {
|
|
24
|
+
const store = await loadTokens();
|
|
25
|
+
if (!store)
|
|
26
|
+
return '';
|
|
27
|
+
if (store.defaultOrgId && store.tokensByOrg[store.defaultOrgId])
|
|
28
|
+
return store.defaultOrgId;
|
|
29
|
+
return Object.keys(store.tokensByOrg)[0] ?? '';
|
|
30
|
+
}
|
|
31
|
+
export async function getValidAccessToken(workosOrgId, log = noopLog) {
|
|
14
32
|
const envKey = process.env.QUONFIG_API_KEY;
|
|
15
33
|
if (envKey && envKey.length > 0) {
|
|
16
34
|
if (!envKey.startsWith('qf_uk_')) {
|
|
@@ -19,66 +37,65 @@ export async function getValidAccessToken(log = noopLog) {
|
|
|
19
37
|
log('getValidAccessToken: using QUONFIG_API_KEY (CI path)');
|
|
20
38
|
return envKey;
|
|
21
39
|
}
|
|
22
|
-
const
|
|
40
|
+
const store = await loadTokens();
|
|
41
|
+
const tokens = store ? getTokenForOrg(store, workosOrgId) : undefined;
|
|
23
42
|
log('getValidAccessToken: loaded tokens', {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
workosOrgId,
|
|
44
|
+
hasStore: Boolean(store),
|
|
45
|
+
hasAccessToken: Boolean(tokens?.access_token),
|
|
46
|
+
hasRefreshToken: Boolean(tokens?.refresh_token),
|
|
47
|
+
expiresAt: tokens?.expires_at,
|
|
28
48
|
});
|
|
29
|
-
if (!tokens?.
|
|
30
|
-
throw new Error(
|
|
49
|
+
if (!store || !tokens?.access_token) {
|
|
50
|
+
throw new Error(`No token for org ${workosOrgId}. Run \`qfg login\` to mint one.`);
|
|
31
51
|
}
|
|
32
|
-
// Check expiry using the JWT's exp claim, falling back to stored
|
|
33
|
-
let expired = tokens.
|
|
52
|
+
// Check expiry using the JWT's exp claim, falling back to stored expires_at
|
|
53
|
+
let expired = tokens.expires_at ? tokens.expires_at < Date.now() : false;
|
|
34
54
|
let expirySource = 'stored';
|
|
35
55
|
try {
|
|
36
|
-
const payload = decodeJWT(tokens.
|
|
56
|
+
const payload = decodeJWT(tokens.access_token);
|
|
37
57
|
if (typeof payload.exp === 'number') {
|
|
38
58
|
expired = payload.exp * 1000 < Date.now();
|
|
39
59
|
expirySource = 'jwt';
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
62
|
catch {
|
|
43
|
-
/* use stored
|
|
63
|
+
/* use stored expires_at */
|
|
44
64
|
}
|
|
45
|
-
log('getValidAccessToken: expiry check', { expired, expirySource, now: Date.now() });
|
|
65
|
+
log('getValidAccessToken: expiry check', { workosOrgId, expired, expirySource, now: Date.now() });
|
|
46
66
|
if (!expired) {
|
|
47
|
-
return tokens.
|
|
67
|
+
return tokens.access_token;
|
|
48
68
|
}
|
|
49
|
-
if (!tokens.
|
|
50
|
-
throw new Error(
|
|
69
|
+
if (!tokens.refresh_token) {
|
|
70
|
+
throw new Error(`Session expired for org ${workosOrgId} (no refresh_token on disk). Run \`qfg login\` to re-authenticate.`);
|
|
51
71
|
}
|
|
52
|
-
log('getValidAccessToken: access token expired, calling WorkOS refresh');
|
|
72
|
+
log('getValidAccessToken: access token expired, calling WorkOS refresh', { workosOrgId });
|
|
53
73
|
let refreshed;
|
|
54
74
|
try {
|
|
55
|
-
|
|
75
|
+
// authenticateWithOrg passes organization_id so the new access token is
|
|
76
|
+
// org-scoped and carries the user's permissions[] for that org. Refreshing
|
|
77
|
+
// without organization_id would mint a user-scoped token (the qfg-kr7
|
|
78
|
+
// root cause) that fails every write path.
|
|
79
|
+
refreshed = await authenticateWithOrg(tokens.refresh_token, workosOrgId);
|
|
56
80
|
}
|
|
57
81
|
catch (error) {
|
|
58
82
|
const detail = error instanceof Error ? error.message : String(error);
|
|
59
|
-
log('getValidAccessToken: refresh FAILED', { detail });
|
|
60
|
-
throw new Error(`Token refresh failed: ${detail}`);
|
|
83
|
+
log('getValidAccessToken: refresh FAILED', { workosOrgId, detail });
|
|
84
|
+
throw new Error(`Token refresh failed for org ${workosOrgId}: ${detail}`);
|
|
61
85
|
}
|
|
62
86
|
log('getValidAccessToken: refresh succeeded', {
|
|
87
|
+
workosOrgId,
|
|
63
88
|
gotNewRefreshToken: Boolean(refreshed.refresh_token),
|
|
64
89
|
});
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const payload = decodeJWT(refreshed.access_token);
|
|
68
|
-
if (typeof payload.exp === 'number') {
|
|
69
|
-
expiresAt = payload.exp * 1000;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* use fallback */
|
|
74
|
-
}
|
|
75
|
-
const updated = {
|
|
90
|
+
store.tokensByOrg[workosOrgId] = {
|
|
76
91
|
...tokens,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
access_token: refreshed.access_token,
|
|
93
|
+
expires_at: refreshed.expires_at,
|
|
94
|
+
refresh_token: refreshed.refresh_token,
|
|
95
|
+
user_email: refreshed.user_email ?? tokens.user_email,
|
|
96
|
+
user_id: refreshed.user_id ?? tokens.user_id,
|
|
80
97
|
};
|
|
81
|
-
await saveTokens(
|
|
82
|
-
log('getValidAccessToken: refreshed tokens saved', { newExpiresAt:
|
|
98
|
+
await saveTokens(store);
|
|
99
|
+
log('getValidAccessToken: refreshed tokens saved', { workosOrgId, newExpiresAt: refreshed.expires_at });
|
|
83
100
|
return refreshed.access_token;
|
|
84
101
|
}
|
package/dist/util/git-ops.d.ts
CHANGED
|
@@ -1,11 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Args prepended to every git invocation. Empty `credential.helper` resets the
|
|
3
|
+
* helper chain for this child process only — prevents macOS git's osxkeychain
|
|
4
|
+
* helper from popping a "Keychain Not Found" dialog when qfg-managed creds are
|
|
5
|
+
* embedded in the URL.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GIT_SAFE_ARGS: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Env additions for every git invocation. Suppress tty prompts and Git
|
|
10
|
+
* Credential Manager interactive flows.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GIT_SAFE_ENV: Readonly<NodeJS.ProcessEnv>;
|
|
1
13
|
/**
|
|
2
14
|
* Redact a token from a URL string so it is safe to display to users.
|
|
3
15
|
*/
|
|
4
16
|
export declare const redactToken: (url: string) => string;
|
|
17
|
+
export interface RunGitOptions {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Extra env vars merged on top of `process.env` and `GIT_SAFE_ENV`. */
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Canonical entry point for shelling out to git from the CLI. Always prepends
|
|
24
|
+
* `GIT_SAFE_ARGS` and merges `GIT_SAFE_ENV` so credential prompts never leak
|
|
25
|
+
* to the user. Errors are re-thrown with tokens redacted from message/stderr.
|
|
26
|
+
*/
|
|
27
|
+
export declare const runGit: (args: string[], options?: RunGitOptions) => Promise<{
|
|
28
|
+
stdout: string;
|
|
29
|
+
stderr: string;
|
|
30
|
+
}>;
|
|
31
|
+
export interface SpawnGitOptions {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
/** If set, written to the child's stdin and then stdin is closed. */
|
|
35
|
+
stdin?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Spawn-based git invocation for cases where stdin needs to be piped (e.g.
|
|
39
|
+
* `git commit -F -`). Same safe-args/env injection as `runGit`.
|
|
40
|
+
*/
|
|
41
|
+
export declare const spawnGit: (args: string[], options?: SpawnGitOptions) => Promise<void>;
|
|
5
42
|
export declare const gitClone: (repoUrl: string, dir: string) => Promise<void>;
|
|
6
43
|
export declare const isGitRepo: (dir: string) => Promise<boolean>;
|
|
7
44
|
export declare const getRemoteUrl: (dir: string) => Promise<string | null>;
|
|
8
45
|
export declare const isWorkingTreeClean: (dir: string) => Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Returns true if `file` (relative to `dir`) has any working-tree or
|
|
48
|
+
* staged change, false if its tracked content matches HEAD.
|
|
49
|
+
*/
|
|
50
|
+
export declare const hasFileChanges: (dir: string, file: string) => Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the list of tracked files (relative paths) with working-tree
|
|
53
|
+
* or staged modifications. Untracked files (`??`) are excluded — they
|
|
54
|
+
* are not considered "dirty" by callers that want to know whether the
|
|
55
|
+
* user has work-in-progress that should not be swept into a commit.
|
|
56
|
+
*/
|
|
57
|
+
export declare const dirtyTrackedFiles: (dir: string) => Promise<string[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Stage and commit a single file with the given message. The path argument
|
|
60
|
+
* to `git commit` ensures only this file is committed even if other files
|
|
61
|
+
* are already staged in the index.
|
|
62
|
+
*
|
|
63
|
+
* Returns true if a commit was created, false if there was nothing to
|
|
64
|
+
* commit (file matches HEAD already). Throws on real git errors.
|
|
65
|
+
*/
|
|
66
|
+
export declare const addAndCommitFile: (dir: string, file: string, message: string) => Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Read the contents of `file` (relative to `dir`) at HEAD. Returns
|
|
69
|
+
* `undefined` if the file does not exist at HEAD or if HEAD itself is
|
|
70
|
+
* unset (empty repo).
|
|
71
|
+
*/
|
|
72
|
+
export declare const readFileAtHead: (dir: string, file: string) => Promise<string | undefined>;
|
|
73
|
+
/**
|
|
74
|
+
* Result of `commitPinFixIfPinOnly`. Tells callers whether a commit was
|
|
75
|
+
* made and, if not, why — useful for verbose logging without throwing on
|
|
76
|
+
* the migration path.
|
|
77
|
+
*/
|
|
78
|
+
export type PinFixResult = {
|
|
79
|
+
kind: 'committed';
|
|
80
|
+
slug: string;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'clean';
|
|
83
|
+
} | {
|
|
84
|
+
kind: 'skipped';
|
|
85
|
+
reason: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Migration helper for legacy state where `qfg pull` wrote the workspace
|
|
89
|
+
* pin to the working tree but never committed it (qfg-0fn). If
|
|
90
|
+
* `quonfig.json` is dirty AND its only diff vs HEAD is an added or
|
|
91
|
+
* changed `workspace` key matching `expectedSlug`, stage and commit the
|
|
92
|
+
* file so push's HEAD-vs-origin delta picks it up.
|
|
93
|
+
*
|
|
94
|
+
* Skips (returns `kind: 'skipped'`) when the dirty file has any other
|
|
95
|
+
* changes, when the pin doesn't match the backend slug, or when JSON
|
|
96
|
+
* parsing fails — leaving the user's working tree alone.
|
|
97
|
+
*/
|
|
98
|
+
export declare const commitPinFixIfPinOnly: (dir: string, file: string, expectedSlug: string) => Promise<PinFixResult>;
|
|
9
99
|
export declare const gitFetch: (dir: string) => Promise<void>;
|
|
10
100
|
/**
|
|
11
101
|
* Returns true if origin/main has commits that can be fast-forwarded into the local branch.
|