chief-clancy 0.2.0 → 0.3.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/README.md +13 -24
- package/dist/bundle/clancy-afk.js +101 -0
- package/dist/bundle/clancy-once.js +13902 -0
- package/dist/installer/file-ops/file-ops.d.ts +32 -0
- package/dist/installer/file-ops/file-ops.d.ts.map +1 -0
- package/dist/installer/file-ops/file-ops.js +58 -0
- package/dist/installer/file-ops/file-ops.js.map +1 -0
- package/dist/installer/hook-installer/hook-installer.d.ts +29 -0
- package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -0
- package/dist/installer/hook-installer/hook-installer.js +96 -0
- package/dist/installer/hook-installer/hook-installer.js.map +1 -0
- package/dist/installer/install.d.ts +3 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +248 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/installer/manifest/manifest.d.ts +41 -0
- package/dist/installer/manifest/manifest.d.ts.map +1 -0
- package/dist/installer/manifest/manifest.js +97 -0
- package/dist/installer/manifest/manifest.js.map +1 -0
- package/dist/installer/prompts/prompts.d.ts +33 -0
- package/dist/installer/prompts/prompts.d.ts.map +1 -0
- package/dist/installer/prompts/prompts.js +55 -0
- package/dist/installer/prompts/prompts.js.map +1 -0
- package/dist/schemas/env.d.ts +75 -0
- package/dist/schemas/env.d.ts.map +1 -0
- package/dist/schemas/env.js +40 -0
- package/dist/schemas/env.js.map +1 -0
- package/dist/schemas/github.d.ts +27 -0
- package/dist/schemas/github.d.ts.map +1 -0
- package/dist/schemas/github.js +17 -0
- package/dist/schemas/github.js.map +1 -0
- package/dist/schemas/index.d.ts +9 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +5 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/jira.d.ts +37 -0
- package/dist/schemas/jira.d.ts.map +1 -0
- package/dist/schemas/jira.js +37 -0
- package/dist/schemas/jira.js.map +1 -0
- package/dist/schemas/linear.d.ts +67 -0
- package/dist/schemas/linear.d.ts.map +1 -0
- package/dist/schemas/linear.js +50 -0
- package/dist/schemas/linear.js.map +1 -0
- package/dist/scripts/afk/afk.d.ts +21 -0
- package/dist/scripts/afk/afk.d.ts.map +1 -0
- package/dist/scripts/afk/afk.js +124 -0
- package/dist/scripts/afk/afk.js.map +1 -0
- package/dist/scripts/board/github/github.d.ts +40 -0
- package/dist/scripts/board/github/github.d.ts.map +1 -0
- package/dist/scripts/board/github/github.js +121 -0
- package/dist/scripts/board/github/github.js.map +1 -0
- package/dist/scripts/board/jira/jira.d.ts +90 -0
- package/dist/scripts/board/jira/jira.d.ts.map +1 -0
- package/dist/scripts/board/jira/jira.js +251 -0
- package/dist/scripts/board/jira/jira.js.map +1 -0
- package/dist/scripts/board/linear/linear.d.ts +85 -0
- package/dist/scripts/board/linear/linear.d.ts.map +1 -0
- package/dist/scripts/board/linear/linear.js +209 -0
- package/dist/scripts/board/linear/linear.js.map +1 -0
- package/dist/scripts/once/once.d.ts +12 -0
- package/dist/scripts/once/once.d.ts.map +1 -0
- package/dist/scripts/once/once.js +330 -0
- package/dist/scripts/once/once.js.map +1 -0
- package/dist/scripts/shared/branch/branch.d.ts +50 -0
- package/dist/scripts/shared/branch/branch.d.ts.map +1 -0
- package/dist/scripts/shared/branch/branch.js +61 -0
- package/dist/scripts/shared/branch/branch.js.map +1 -0
- package/dist/scripts/shared/claude-cli/claude-cli.d.ts +17 -0
- package/dist/scripts/shared/claude-cli/claude-cli.d.ts.map +1 -0
- package/dist/scripts/shared/claude-cli/claude-cli.js +35 -0
- package/dist/scripts/shared/claude-cli/claude-cli.js.map +1 -0
- package/dist/scripts/shared/env-parser/env-parser.d.ts +30 -0
- package/dist/scripts/shared/env-parser/env-parser.d.ts.map +1 -0
- package/dist/scripts/shared/env-parser/env-parser.js +64 -0
- package/dist/scripts/shared/env-parser/env-parser.js.map +1 -0
- package/dist/scripts/shared/env-schema/env-schema.d.ts +27 -0
- package/dist/scripts/shared/env-schema/env-schema.d.ts.map +1 -0
- package/dist/scripts/shared/env-schema/env-schema.js +46 -0
- package/dist/scripts/shared/env-schema/env-schema.js.map +1 -0
- package/dist/scripts/shared/git-ops/git-ops.d.ts +52 -0
- package/dist/scripts/shared/git-ops/git-ops.d.ts.map +1 -0
- package/dist/scripts/shared/git-ops/git-ops.js +107 -0
- package/dist/scripts/shared/git-ops/git-ops.js.map +1 -0
- package/dist/scripts/shared/http/http.d.ts +52 -0
- package/dist/scripts/shared/http/http.d.ts.map +1 -0
- package/dist/scripts/shared/http/http.js +74 -0
- package/dist/scripts/shared/http/http.js.map +1 -0
- package/dist/scripts/shared/notify/notify.d.ts +46 -0
- package/dist/scripts/shared/notify/notify.d.ts.map +1 -0
- package/dist/scripts/shared/notify/notify.js +88 -0
- package/dist/scripts/shared/notify/notify.js.map +1 -0
- package/dist/scripts/shared/preflight/preflight.d.ts +40 -0
- package/dist/scripts/shared/preflight/preflight.d.ts.map +1 -0
- package/dist/scripts/shared/preflight/preflight.js +84 -0
- package/dist/scripts/shared/preflight/preflight.js.map +1 -0
- package/dist/scripts/shared/progress/progress.d.ts +25 -0
- package/dist/scripts/shared/progress/progress.d.ts.map +1 -0
- package/dist/scripts/shared/progress/progress.js +46 -0
- package/dist/scripts/shared/progress/progress.js.map +1 -0
- package/dist/scripts/shared/prompt/prompt.d.ts +38 -0
- package/dist/scripts/shared/prompt/prompt.d.ts.map +1 -0
- package/dist/scripts/shared/prompt/prompt.js +77 -0
- package/dist/scripts/shared/prompt/prompt.js.map +1 -0
- package/dist/types/board.d.ts +13 -0
- package/dist/types/board.d.ts.map +1 -0
- package/dist/types/board.js +5 -0
- package/dist/types/board.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ansi/ansi.d.ts +55 -0
- package/dist/utils/ansi/ansi.d.ts.map +1 -0
- package/dist/utils/ansi/ansi.js +55 -0
- package/dist/utils/ansi/ansi.js.map +1 -0
- package/dist/utils/parse-json/parse-json.d.ts +20 -0
- package/dist/utils/parse-json/parse-json.d.ts.map +1 -0
- package/dist/utils/parse-json/parse-json.js +27 -0
- package/dist/utils/parse-json/parse-json.js.map +1 -0
- package/hooks/clancy-check-update.js +2 -2
- package/hooks/clancy-credential-guard.js +8 -1
- package/package.json +48 -8
- package/registry/boards.json +3 -6
- package/src/templates/CLAUDE.md +1 -1
- package/src/workflows/doctor.md +32 -23
- package/src/workflows/init.md +101 -26
- package/src/workflows/logs.md +13 -6
- package/src/workflows/map-codebase.md +17 -16
- package/src/workflows/once.md +22 -12
- package/src/workflows/review.md +40 -27
- package/src/workflows/run.md +20 -12
- package/src/workflows/scaffold.md +5 -1034
- package/src/workflows/settings.md +9 -6
- package/src/workflows/status.md +17 -8
- package/src/workflows/uninstall.md +11 -6
- package/src/workflows/update.md +20 -13
- package/bin/install.js +0 -362
- package/src/templates/scripts/clancy-afk.sh +0 -111
- package/src/templates/scripts/clancy-once-github.sh +0 -249
- package/src/templates/scripts/clancy-once-linear.sh +0 -320
- package/src/templates/scripts/clancy-once.sh +0 -322
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Ticket } from '../../../types/index.js';
|
|
2
|
+
type LinearEnv = {
|
|
3
|
+
LINEAR_API_KEY: string;
|
|
4
|
+
LINEAR_TEAM_ID: string;
|
|
5
|
+
CLANCY_LABEL?: string;
|
|
6
|
+
CLANCY_BASE_BRANCH?: string;
|
|
7
|
+
CLANCY_STATUS_IN_PROGRESS?: string;
|
|
8
|
+
CLANCY_STATUS_DONE?: string;
|
|
9
|
+
CLANCY_MODEL?: string;
|
|
10
|
+
CLANCY_NOTIFY_WEBHOOK?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Validate that a team ID is safe for use in GraphQL variables.
|
|
14
|
+
*
|
|
15
|
+
* @param teamId - The Linear team ID to validate.
|
|
16
|
+
* @returns `true` if the ID matches the safe pattern.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isValidTeamId(teamId: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Make a GraphQL request to the Linear API.
|
|
21
|
+
*
|
|
22
|
+
* Personal API keys are passed directly (no "Bearer" prefix).
|
|
23
|
+
*
|
|
24
|
+
* @param apiKey - The Linear personal API key.
|
|
25
|
+
* @param query - The GraphQL query string.
|
|
26
|
+
* @param variables - The GraphQL variables object.
|
|
27
|
+
* @returns The raw JSON response, or `undefined` on failure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function linearGraphql(apiKey: string, query: string, variables?: Record<string, unknown>): Promise<unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Ping the Linear API to verify connectivity and credentials.
|
|
32
|
+
*
|
|
33
|
+
* @param apiKey - The Linear personal API key.
|
|
34
|
+
* @returns An object with `ok` and optional `error` message.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pingLinear(apiKey: string): Promise<{
|
|
37
|
+
ok: boolean;
|
|
38
|
+
error?: string;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the next available issue from Linear.
|
|
42
|
+
*
|
|
43
|
+
* Filters by `state.type: "unstarted"` (enum, works regardless of team
|
|
44
|
+
* column naming) and optionally by label.
|
|
45
|
+
*
|
|
46
|
+
* @param env - The Linear environment variables.
|
|
47
|
+
* @returns The fetched ticket with optional parent info, or `undefined` if none available.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fetchIssue(env: LinearEnv): Promise<(Ticket & {
|
|
50
|
+
issueId: string;
|
|
51
|
+
parentIdentifier?: string;
|
|
52
|
+
}) | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* Look up a Linear workflow state ID by name and team.
|
|
55
|
+
*
|
|
56
|
+
* @param apiKey - The Linear personal API key.
|
|
57
|
+
* @param teamId - The Linear team ID.
|
|
58
|
+
* @param stateName - The workflow state name (e.g., `'In Progress'`).
|
|
59
|
+
* @returns The state ID, or `undefined` if not found.
|
|
60
|
+
*/
|
|
61
|
+
export declare function lookupWorkflowStateId(apiKey: string, teamId: string, stateName: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Execute a state transition on a Linear issue.
|
|
64
|
+
*
|
|
65
|
+
* @param apiKey - The Linear personal API key.
|
|
66
|
+
* @param issueId - The Linear issue internal ID.
|
|
67
|
+
* @param stateId - The target workflow state ID.
|
|
68
|
+
* @returns `true` if the mutation succeeded.
|
|
69
|
+
*/
|
|
70
|
+
export declare function executeStateTransition(apiKey: string, issueId: string, stateId: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Transition a Linear issue to a new workflow state.
|
|
73
|
+
*
|
|
74
|
+
* Looks up the workflow state ID by name, then executes the `issueUpdate` mutation.
|
|
75
|
+
* Best-effort — never throws on failure.
|
|
76
|
+
*
|
|
77
|
+
* @param apiKey - The Linear personal API key.
|
|
78
|
+
* @param teamId - The Linear team ID.
|
|
79
|
+
* @param issueId - The Linear issue internal ID.
|
|
80
|
+
* @param stateName - The target workflow state name (e.g., `'In Progress'`).
|
|
81
|
+
* @returns `true` if the transition succeeded.
|
|
82
|
+
*/
|
|
83
|
+
export declare function transitionIssue(apiKey: string, teamId: string, issueId: string, stateName: string): Promise<boolean>;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=linear.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../../../src/scripts/board/linear/linear.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK/C,KAAK,SAAS,GAAG;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,OAAO,CAAC,CA8BlB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAO1C;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CACrD,CAAC,MAAM,GAAG;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,GACF,SAAS,CACZ,CA0DA;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB7B;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAelB"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clancy Linear board script.
|
|
3
|
+
*
|
|
4
|
+
* Fetches an issue from Linear's GraphQL API, creates branches,
|
|
5
|
+
* invokes Claude, squash merges, transitions status, and sends notifications.
|
|
6
|
+
*
|
|
7
|
+
* Important: Linear personal API keys do NOT use "Bearer" prefix.
|
|
8
|
+
* Only OAuth tokens use "Bearer". This is intentional per Linear docs.
|
|
9
|
+
*/
|
|
10
|
+
import { linearIssueUpdateResponseSchema, linearIssuesResponseSchema, linearViewerResponseSchema, linearWorkflowStatesResponseSchema, } from '../../../schemas/linear.js';
|
|
11
|
+
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
12
|
+
const LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
13
|
+
/**
|
|
14
|
+
* Validate that a team ID is safe for use in GraphQL variables.
|
|
15
|
+
*
|
|
16
|
+
* @param teamId - The Linear team ID to validate.
|
|
17
|
+
* @returns `true` if the ID matches the safe pattern.
|
|
18
|
+
*/
|
|
19
|
+
export function isValidTeamId(teamId) {
|
|
20
|
+
return SAFE_ID_PATTERN.test(teamId);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Make a GraphQL request to the Linear API.
|
|
24
|
+
*
|
|
25
|
+
* Personal API keys are passed directly (no "Bearer" prefix).
|
|
26
|
+
*
|
|
27
|
+
* @param apiKey - The Linear personal API key.
|
|
28
|
+
* @param query - The GraphQL query string.
|
|
29
|
+
* @param variables - The GraphQL variables object.
|
|
30
|
+
* @returns The raw JSON response, or `undefined` on failure.
|
|
31
|
+
*/
|
|
32
|
+
export async function linearGraphql(apiKey, query, variables) {
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await fetch(LINEAR_API_URL, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: apiKey,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({ query, variables }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.warn(`⚠ Linear API request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
console.warn(`⚠ Linear API returned HTTP ${response.status}`);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return await response.json();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
console.warn('⚠ Linear API returned invalid JSON');
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ping the Linear API to verify connectivity and credentials.
|
|
62
|
+
*
|
|
63
|
+
* @param apiKey - The Linear personal API key.
|
|
64
|
+
* @returns An object with `ok` and optional `error` message.
|
|
65
|
+
*/
|
|
66
|
+
export async function pingLinear(apiKey) {
|
|
67
|
+
const raw = await linearGraphql(apiKey, '{ viewer { id } }');
|
|
68
|
+
const parsed = linearViewerResponseSchema.safeParse(raw);
|
|
69
|
+
if (parsed.success && parsed.data.data?.viewer?.id)
|
|
70
|
+
return { ok: true };
|
|
71
|
+
return { ok: false, error: '✗ Linear auth failed — check LINEAR_API_KEY' };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Fetch the next available issue from Linear.
|
|
75
|
+
*
|
|
76
|
+
* Filters by `state.type: "unstarted"` (enum, works regardless of team
|
|
77
|
+
* column naming) and optionally by label.
|
|
78
|
+
*
|
|
79
|
+
* @param env - The Linear environment variables.
|
|
80
|
+
* @returns The fetched ticket with optional parent info, or `undefined` if none available.
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchIssue(env) {
|
|
83
|
+
const label = env.CLANCY_LABEL?.trim();
|
|
84
|
+
const hasLabel = Boolean(label);
|
|
85
|
+
const labelFilter = hasLabel ? 'labels: { name: { eq: $label } }' : '';
|
|
86
|
+
const query = `
|
|
87
|
+
query($teamId: String!${hasLabel ? ', $label: String!' : ''}) {
|
|
88
|
+
viewer {
|
|
89
|
+
assignedIssues(
|
|
90
|
+
filter: {
|
|
91
|
+
state: { type: { eq: "unstarted" } }
|
|
92
|
+
team: { id: { eq: $teamId } }
|
|
93
|
+
${labelFilter}
|
|
94
|
+
}
|
|
95
|
+
first: 1
|
|
96
|
+
orderBy: priority
|
|
97
|
+
) {
|
|
98
|
+
nodes {
|
|
99
|
+
id
|
|
100
|
+
identifier
|
|
101
|
+
title
|
|
102
|
+
description
|
|
103
|
+
parent { identifier title }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
const variables = {
|
|
110
|
+
teamId: env.LINEAR_TEAM_ID,
|
|
111
|
+
};
|
|
112
|
+
if (hasLabel)
|
|
113
|
+
variables.label = label;
|
|
114
|
+
const raw = await linearGraphql(env.LINEAR_API_KEY, query, variables);
|
|
115
|
+
const parsed = linearIssuesResponseSchema.safeParse(raw);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
console.warn(`⚠ Unexpected Linear response shape: ${parsed.error.message}`);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const nodes = parsed.data.data?.viewer?.assignedIssues?.nodes;
|
|
121
|
+
if (!nodes?.length)
|
|
122
|
+
return undefined;
|
|
123
|
+
const issue = nodes[0];
|
|
124
|
+
return {
|
|
125
|
+
key: issue.identifier,
|
|
126
|
+
title: issue.title,
|
|
127
|
+
description: issue.description ?? '',
|
|
128
|
+
provider: 'linear',
|
|
129
|
+
issueId: issue.id,
|
|
130
|
+
parentIdentifier: issue.parent?.identifier,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Look up a Linear workflow state ID by name and team.
|
|
135
|
+
*
|
|
136
|
+
* @param apiKey - The Linear personal API key.
|
|
137
|
+
* @param teamId - The Linear team ID.
|
|
138
|
+
* @param stateName - The workflow state name (e.g., `'In Progress'`).
|
|
139
|
+
* @returns The state ID, or `undefined` if not found.
|
|
140
|
+
*/
|
|
141
|
+
export async function lookupWorkflowStateId(apiKey, teamId, stateName) {
|
|
142
|
+
const query = `
|
|
143
|
+
query($teamId: String!, $name: String!) {
|
|
144
|
+
workflowStates(filter: {
|
|
145
|
+
team: { id: { eq: $teamId } }
|
|
146
|
+
name: { eq: $name }
|
|
147
|
+
}) {
|
|
148
|
+
nodes { id }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
const raw = await linearGraphql(apiKey, query, { teamId, name: stateName });
|
|
153
|
+
const parsed = linearWorkflowStatesResponseSchema.safeParse(raw);
|
|
154
|
+
if (!parsed.success) {
|
|
155
|
+
console.warn(`⚠ Unexpected Linear workflowStates response: ${parsed.error.message}`);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return parsed.data.data?.workflowStates?.nodes?.[0]?.id;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Execute a state transition on a Linear issue.
|
|
162
|
+
*
|
|
163
|
+
* @param apiKey - The Linear personal API key.
|
|
164
|
+
* @param issueId - The Linear issue internal ID.
|
|
165
|
+
* @param stateId - The target workflow state ID.
|
|
166
|
+
* @returns `true` if the mutation succeeded.
|
|
167
|
+
*/
|
|
168
|
+
export async function executeStateTransition(apiKey, issueId, stateId) {
|
|
169
|
+
const mutation = `
|
|
170
|
+
mutation($issueId: String!, $stateId: String!) {
|
|
171
|
+
issueUpdate(id: $issueId, input: { stateId: $stateId }) {
|
|
172
|
+
success
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
const raw = await linearGraphql(apiKey, mutation, { issueId, stateId });
|
|
177
|
+
const parsed = linearIssueUpdateResponseSchema.safeParse(raw);
|
|
178
|
+
if (!parsed.success) {
|
|
179
|
+
console.warn(`⚠ Unexpected Linear issueUpdate response: ${parsed.error.message}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return parsed.data.data?.issueUpdate?.success === true;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Transition a Linear issue to a new workflow state.
|
|
186
|
+
*
|
|
187
|
+
* Looks up the workflow state ID by name, then executes the `issueUpdate` mutation.
|
|
188
|
+
* Best-effort — never throws on failure.
|
|
189
|
+
*
|
|
190
|
+
* @param apiKey - The Linear personal API key.
|
|
191
|
+
* @param teamId - The Linear team ID.
|
|
192
|
+
* @param issueId - The Linear issue internal ID.
|
|
193
|
+
* @param stateName - The target workflow state name (e.g., `'In Progress'`).
|
|
194
|
+
* @returns `true` if the transition succeeded.
|
|
195
|
+
*/
|
|
196
|
+
export async function transitionIssue(apiKey, teamId, issueId, stateName) {
|
|
197
|
+
try {
|
|
198
|
+
const stateId = await lookupWorkflowStateId(apiKey, teamId, stateName);
|
|
199
|
+
if (!stateId) {
|
|
200
|
+
console.warn(`⚠ Linear workflow state "${stateName}" not found — check team configuration`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return await executeStateTransition(apiKey, issueId, stateId);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=linear.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear.js","sourceRoot":"","sources":["../../../../src/scripts/board/linear/linear.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the once orchestrator — full ticket lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* @param argv - Process arguments (supports `--dry-run` flag).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* await run(process.argv);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function run(argv: string[]): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=once.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"once.d.ts","sourceRoot":"","sources":["../../../src/scripts/once/once.ts"],"names":[],"mappings":"AAuPA;;;;;;;;;GASG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0NvD"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified once orchestrator — replaces all three `clancy-once-*.sh` scripts.
|
|
3
|
+
*
|
|
4
|
+
* Full lifecycle: preflight → detect board → fetch ticket → compute branches →
|
|
5
|
+
* [dry-run gate] → transition In Progress → create branch → invoke Claude →
|
|
6
|
+
* squash merge → transition Done → log → notify.
|
|
7
|
+
*
|
|
8
|
+
* All errors exit with code 0 (not 1). This is intentional — the AFK runner
|
|
9
|
+
* detects stop conditions by parsing stdout, not exit codes.
|
|
10
|
+
*/
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { closeIssue, fetchIssue as fetchGitHubIssue, isValidRepo, pingGitHub, } from '../../scripts/board/github/github.js';
|
|
14
|
+
import { buildAuthHeader, fetchTicket as fetchJiraTicket, isSafeJqlValue, pingJira, transitionIssue as transitionJiraIssue, } from '../../scripts/board/jira/jira.js';
|
|
15
|
+
import { fetchIssue as fetchLinearIssue, isValidTeamId, pingLinear, transitionIssue as transitionLinearIssue, } from '../../scripts/board/linear/linear.js';
|
|
16
|
+
import { computeTargetBranch, computeTicketBranch, } from '../../scripts/shared/branch/branch.js';
|
|
17
|
+
import { invokeClaudeSession } from '../../scripts/shared/claude-cli/claude-cli.js';
|
|
18
|
+
import { detectBoard } from '../../scripts/shared/env-schema/env-schema.js';
|
|
19
|
+
import { checkout, currentBranch, deleteBranch, ensureBranch, squashMerge, } from '../../scripts/shared/git-ops/git-ops.js';
|
|
20
|
+
import { sendNotification } from '../../scripts/shared/notify/notify.js';
|
|
21
|
+
import { runPreflight } from '../../scripts/shared/preflight/preflight.js';
|
|
22
|
+
import { appendProgress } from '../../scripts/shared/progress/progress.js';
|
|
23
|
+
import { buildPrompt } from '../../scripts/shared/prompt/prompt.js';
|
|
24
|
+
import { bold, dim, green, red, yellow } from '../../utils/ansi/ansi.js';
|
|
25
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
26
|
+
function formatDuration(ms) {
|
|
27
|
+
const secs = Math.floor(ms / 1000);
|
|
28
|
+
if (secs < 60)
|
|
29
|
+
return `${secs}s`;
|
|
30
|
+
const mins = Math.floor(secs / 60);
|
|
31
|
+
const remSecs = secs % 60;
|
|
32
|
+
return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`;
|
|
33
|
+
}
|
|
34
|
+
// ─── Board-specific fetch ────────────────────────────────────────────────────
|
|
35
|
+
async function fetchTicket(config) {
|
|
36
|
+
switch (config.provider) {
|
|
37
|
+
case 'jira': {
|
|
38
|
+
const { env } = config;
|
|
39
|
+
const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
|
|
40
|
+
const ticket = await fetchJiraTicket(env.JIRA_BASE_URL, auth, env.JIRA_PROJECT_KEY, env.CLANCY_JQL_STATUS ?? 'To Do', env.CLANCY_JQL_SPRINT, env.CLANCY_LABEL);
|
|
41
|
+
if (!ticket)
|
|
42
|
+
return undefined;
|
|
43
|
+
const blockerStr = ticket.blockers.length
|
|
44
|
+
? `Blocked by: ${ticket.blockers.join(', ')}`
|
|
45
|
+
: 'None';
|
|
46
|
+
return {
|
|
47
|
+
key: ticket.key,
|
|
48
|
+
title: ticket.title,
|
|
49
|
+
description: ticket.description,
|
|
50
|
+
parentInfo: ticket.epicKey ?? 'none',
|
|
51
|
+
blockers: blockerStr,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case 'github': {
|
|
55
|
+
const { env } = config;
|
|
56
|
+
const ticket = await fetchGitHubIssue(env.GITHUB_TOKEN, env.GITHUB_REPO, env.CLANCY_LABEL);
|
|
57
|
+
if (!ticket)
|
|
58
|
+
return undefined;
|
|
59
|
+
return {
|
|
60
|
+
key: ticket.key,
|
|
61
|
+
title: ticket.title,
|
|
62
|
+
description: ticket.description,
|
|
63
|
+
parentInfo: ticket.milestone ?? 'none',
|
|
64
|
+
blockers: 'None',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
case 'linear': {
|
|
68
|
+
const { env } = config;
|
|
69
|
+
const ticket = await fetchLinearIssue({
|
|
70
|
+
LINEAR_API_KEY: env.LINEAR_API_KEY,
|
|
71
|
+
LINEAR_TEAM_ID: env.LINEAR_TEAM_ID,
|
|
72
|
+
CLANCY_LABEL: env.CLANCY_LABEL,
|
|
73
|
+
});
|
|
74
|
+
if (!ticket)
|
|
75
|
+
return undefined;
|
|
76
|
+
return {
|
|
77
|
+
key: ticket.key,
|
|
78
|
+
title: ticket.title,
|
|
79
|
+
description: ticket.description,
|
|
80
|
+
parentInfo: ticket.parentIdentifier ?? 'none',
|
|
81
|
+
blockers: 'None',
|
|
82
|
+
linearIssueId: ticket.issueId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ─── Board-specific ping ─────────────────────────────────────────────────────
|
|
88
|
+
async function pingBoard(config) {
|
|
89
|
+
switch (config.provider) {
|
|
90
|
+
case 'jira': {
|
|
91
|
+
const { env } = config;
|
|
92
|
+
const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
|
|
93
|
+
return pingJira(env.JIRA_BASE_URL, env.JIRA_PROJECT_KEY, auth);
|
|
94
|
+
}
|
|
95
|
+
case 'github':
|
|
96
|
+
return pingGitHub(config.env.GITHUB_TOKEN, config.env.GITHUB_REPO);
|
|
97
|
+
case 'linear':
|
|
98
|
+
return pingLinear(config.env.LINEAR_API_KEY);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ─── Board-specific validation ───────────────────────────────────────────────
|
|
102
|
+
function validateInputs(config) {
|
|
103
|
+
switch (config.provider) {
|
|
104
|
+
case 'jira': {
|
|
105
|
+
const { env } = config;
|
|
106
|
+
if (!isSafeJqlValue(env.JIRA_PROJECT_KEY)) {
|
|
107
|
+
return '✗ JIRA_PROJECT_KEY contains invalid characters';
|
|
108
|
+
}
|
|
109
|
+
if (env.CLANCY_LABEL && !isSafeJqlValue(env.CLANCY_LABEL)) {
|
|
110
|
+
return '✗ CLANCY_LABEL contains invalid characters';
|
|
111
|
+
}
|
|
112
|
+
if (env.CLANCY_JQL_STATUS && !isSafeJqlValue(env.CLANCY_JQL_STATUS)) {
|
|
113
|
+
return '✗ CLANCY_JQL_STATUS contains invalid characters';
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
case 'github': {
|
|
118
|
+
if (!isValidRepo(config.env.GITHUB_REPO)) {
|
|
119
|
+
return '✗ GITHUB_REPO format is invalid — expected owner/repo';
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
case 'linear': {
|
|
124
|
+
if (!isValidTeamId(config.env.LINEAR_TEAM_ID)) {
|
|
125
|
+
return '✗ LINEAR_TEAM_ID contains invalid characters';
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ─── Board-specific transitions ──────────────────────────────────────────────
|
|
132
|
+
async function transitionToStatus(config, ticket, statusName) {
|
|
133
|
+
switch (config.provider) {
|
|
134
|
+
case 'jira': {
|
|
135
|
+
const { env } = config;
|
|
136
|
+
const auth = buildAuthHeader(env.JIRA_USER, env.JIRA_API_TOKEN);
|
|
137
|
+
const issueKey = ticket.key;
|
|
138
|
+
const ok = await transitionJiraIssue(env.JIRA_BASE_URL, auth, issueKey, statusName);
|
|
139
|
+
if (ok)
|
|
140
|
+
console.log(` → Transitioned to ${statusName}`);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'github': {
|
|
144
|
+
// GitHub Issues only has open/closed — status transitions not applicable
|
|
145
|
+
// closeIssue is called separately after merge
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'linear': {
|
|
149
|
+
const { env } = config;
|
|
150
|
+
if (!ticket.linearIssueId)
|
|
151
|
+
break;
|
|
152
|
+
const ok = await transitionLinearIssue(env.LINEAR_API_KEY, env.LINEAR_TEAM_ID, ticket.linearIssueId, statusName);
|
|
153
|
+
if (ok)
|
|
154
|
+
console.log(` → Transitioned to ${statusName}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ─── Main orchestrator ───────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Run the once orchestrator — full ticket lifecycle.
|
|
162
|
+
*
|
|
163
|
+
* @param argv - Process arguments (supports `--dry-run` flag).
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* await run(process.argv);
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export async function run(argv) {
|
|
171
|
+
const dryRun = argv.includes('--dry-run');
|
|
172
|
+
const startTime = Date.now();
|
|
173
|
+
console.log(dim('┌──────────────────────────────────────┐'));
|
|
174
|
+
console.log(dim('│') + bold(' 🤖 Clancy — once mode ') + dim('│'));
|
|
175
|
+
console.log(dim('│') + dim(' "Let\'s roll." ') + dim('│'));
|
|
176
|
+
console.log(dim('└──────────────────────────────────────┘'));
|
|
177
|
+
console.log('');
|
|
178
|
+
let originalBranch;
|
|
179
|
+
try {
|
|
180
|
+
// 1. Preflight
|
|
181
|
+
const preflight = runPreflight(process.cwd());
|
|
182
|
+
if (!preflight.ok) {
|
|
183
|
+
console.log(preflight.error);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (preflight.warning) {
|
|
187
|
+
console.log(preflight.warning);
|
|
188
|
+
}
|
|
189
|
+
// 2. Detect board
|
|
190
|
+
const boardResult = detectBoard(preflight.env);
|
|
191
|
+
if (typeof boardResult === 'string') {
|
|
192
|
+
console.log(boardResult);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const config = boardResult;
|
|
196
|
+
// 3. Validate board-specific inputs
|
|
197
|
+
const validationError = validateInputs(config);
|
|
198
|
+
if (validationError) {
|
|
199
|
+
console.log(validationError);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// 4. Ping board
|
|
203
|
+
const ping = await pingBoard(config);
|
|
204
|
+
if (!ping.ok) {
|
|
205
|
+
console.log(ping.error);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log(green('✅ Preflight passed'));
|
|
209
|
+
// 5. Fetch ticket
|
|
210
|
+
const ticket = await fetchTicket(config);
|
|
211
|
+
if (!ticket) {
|
|
212
|
+
console.log(dim('No tickets found. All done!'));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// 6. Compute branches
|
|
216
|
+
const baseBranch = config.env.CLANCY_BASE_BRANCH ?? 'main';
|
|
217
|
+
const parent = ticket.parentInfo !== 'none' ? ticket.parentInfo : undefined;
|
|
218
|
+
const ticketBranch = computeTicketBranch(config.provider, ticket.key);
|
|
219
|
+
const targetBranch = computeTargetBranch(config.provider, baseBranch, parent);
|
|
220
|
+
// 7. Dry-run gate
|
|
221
|
+
if (dryRun) {
|
|
222
|
+
const parentLabel = config.provider === 'github' ? 'Milestone' : 'Epic';
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(yellow('── Dry Run ──────────────────────────────────────'));
|
|
225
|
+
console.log(` Ticket: ${bold(`[${ticket.key}]`)} ${ticket.title}`);
|
|
226
|
+
console.log(` ${parentLabel}:${' '.repeat(14 - parentLabel.length)}${ticket.parentInfo}`);
|
|
227
|
+
if (config.provider !== 'github') {
|
|
228
|
+
console.log(` Blockers: ${ticket.blockers}`);
|
|
229
|
+
}
|
|
230
|
+
console.log(` Target branch: ${ticketBranch} → ${targetBranch}`);
|
|
231
|
+
console.log(yellow('─────────────────────────────────────────────────'));
|
|
232
|
+
console.log(dim(' No changes made. Remove --dry-run to run for real.'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// 8. Print ticket info
|
|
236
|
+
const parentLabel = config.provider === 'github' ? 'Milestone' : 'Epic';
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log(`🎫 ${bold(`[${ticket.key}]`)} ${ticket.title}`);
|
|
239
|
+
console.log(dim(` ${parentLabel}: ${ticket.parentInfo} | Branch: ${ticketBranch} → ${targetBranch}`));
|
|
240
|
+
if (config.provider !== 'github' && ticket.blockers !== 'None') {
|
|
241
|
+
console.log(yellow(` Blockers: ${ticket.blockers}`));
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
// 9. Git: set up branches
|
|
245
|
+
originalBranch = currentBranch();
|
|
246
|
+
ensureBranch(targetBranch, baseBranch);
|
|
247
|
+
checkout(targetBranch);
|
|
248
|
+
checkout(ticketBranch, true);
|
|
249
|
+
// 10. Transition to In Progress (best-effort)
|
|
250
|
+
const statusInProgress = config.env.CLANCY_STATUS_IN_PROGRESS;
|
|
251
|
+
if (statusInProgress) {
|
|
252
|
+
await transitionToStatus(config, ticket, statusInProgress);
|
|
253
|
+
}
|
|
254
|
+
// 11. Build prompt and invoke Claude
|
|
255
|
+
const prompt = buildPrompt({
|
|
256
|
+
provider: config.provider,
|
|
257
|
+
key: ticket.key,
|
|
258
|
+
title: ticket.title,
|
|
259
|
+
description: ticket.description,
|
|
260
|
+
parentInfo: ticket.parentInfo,
|
|
261
|
+
blockers: config.provider !== 'github' ? ticket.blockers : undefined,
|
|
262
|
+
});
|
|
263
|
+
const claudeOk = invokeClaudeSession(prompt, config.env.CLANCY_MODEL);
|
|
264
|
+
if (!claudeOk) {
|
|
265
|
+
console.log(yellow('⚠ Claude session exited with an error. Skipping merge.'));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// 12. Squash merge
|
|
269
|
+
checkout(targetBranch);
|
|
270
|
+
const commitMsg = `feat(${ticket.key}): ${ticket.title}`;
|
|
271
|
+
const hadChanges = squashMerge(ticketBranch, commitMsg);
|
|
272
|
+
if (!hadChanges) {
|
|
273
|
+
console.log(yellow('⚠ No changes staged after squash merge. Claude may not have committed any work.'));
|
|
274
|
+
}
|
|
275
|
+
// 13. Delete feature branch
|
|
276
|
+
deleteBranch(ticketBranch);
|
|
277
|
+
// 14. Transition to Done / close issue (best-effort)
|
|
278
|
+
const statusDone = config.env.CLANCY_STATUS_DONE;
|
|
279
|
+
if (config.provider === 'github') {
|
|
280
|
+
const issueNumber = parseInt(ticket.key.replace('#', ''), 10);
|
|
281
|
+
if (Number.isNaN(issueNumber)) {
|
|
282
|
+
console.log(`⚠ Could not parse issue number from ${ticket.key}. Close it manually on GitHub.`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const closed = await closeIssue(config.env.GITHUB_TOKEN, config.env.GITHUB_REPO, issueNumber);
|
|
286
|
+
if (!closed) {
|
|
287
|
+
console.log(`⚠ Could not close issue ${ticket.key}. Close it manually on GitHub.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (statusDone) {
|
|
292
|
+
await transitionToStatus(config, ticket, statusDone);
|
|
293
|
+
}
|
|
294
|
+
// 15. Log progress
|
|
295
|
+
appendProgress(process.cwd(), ticket.key, ticket.title, 'DONE');
|
|
296
|
+
const elapsed = formatDuration(Date.now() - startTime);
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(green(`🏁 ${ticket.key} complete`) + dim(` (${elapsed})`));
|
|
299
|
+
console.log(dim(' "Bake \'em away, toys."'));
|
|
300
|
+
// 16. Send notification (best-effort)
|
|
301
|
+
const webhook = config.env.CLANCY_NOTIFY_WEBHOOK;
|
|
302
|
+
if (webhook) {
|
|
303
|
+
await sendNotification(webhook, `✓ Clancy completed [${ticket.key}] ${ticket.title}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
// Unexpected errors — print and exit cleanly (exit 0 for AFK loop compat)
|
|
308
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
309
|
+
const elapsed = formatDuration(Date.now() - startTime);
|
|
310
|
+
console.error('');
|
|
311
|
+
console.error(red(`❌ Clancy stopped`) + dim(` (${elapsed})`));
|
|
312
|
+
console.error(red(` ${msg}`));
|
|
313
|
+
console.error(dim(' "I\'d rather let Herman go."'));
|
|
314
|
+
// Best-effort: restore the branch the user was on before Clancy started
|
|
315
|
+
if (originalBranch) {
|
|
316
|
+
try {
|
|
317
|
+
checkout(originalBranch);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Ignore — branch restore is best-effort
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Main guard — self-execute when run directly (e.g. node .clancy/clancy-once.js)
|
|
326
|
+
if (process.argv[1] &&
|
|
327
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
328
|
+
run(process.argv);
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=once.js.map
|