@spekn/cli 1.0.0 → 1.0.2

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.
Files changed (154) hide show
  1. package/README.md +58 -0
  2. package/dist/main.js +40540 -32176
  3. package/dist/prompts/governance-analysis.prompt.md +109 -0
  4. package/dist/resources/prompts/repo-analysis.prompt.md +28 -136
  5. package/dist/resources/prompts/repo-sync-analysis.prompt.md +31 -68
  6. package/dist/tui/chunk-4WEASLXY.mjs +3444 -0
  7. package/dist/tui/chunk-755CADEG.mjs +3401 -0
  8. package/dist/tui/chunk-BUJQVTY5.mjs +3409 -0
  9. package/dist/tui/chunk-BZKKMGFB.mjs +1959 -0
  10. package/dist/tui/chunk-DJYOBCNM.mjs +3159 -0
  11. package/dist/tui/chunk-GTFTFDY4.mjs +3417 -0
  12. package/dist/tui/chunk-IMEBD2KA.mjs +3444 -0
  13. package/dist/tui/chunk-IX6DR5SW.mjs +3433 -0
  14. package/dist/tui/chunk-JKFOY4IF.mjs +2003 -0
  15. package/dist/tui/chunk-OXXZ3O5L.mjs +3378 -0
  16. package/dist/tui/chunk-SHJNIAAJ.mjs +1697 -0
  17. package/dist/tui/chunk-V4SNDRUS.mjs +1666 -0
  18. package/dist/tui/chunk-VXVHNZST.mjs +1666 -0
  19. package/dist/tui/chunk-WCTSFKTA.mjs +3459 -0
  20. package/dist/tui/chunk-X2XP5ACW.mjs +3443 -0
  21. package/dist/tui/chunk-YUYJ7VBG.mjs +2029 -0
  22. package/dist/tui/chunk-ZM3EI5IA.mjs +3384 -0
  23. package/dist/tui/chunk-ZYOX64HP.mjs +1653 -0
  24. package/dist/tui/index.mjs +6999 -6938
  25. package/dist/tui/prompts/spec-creation-system.prompt.md +47 -0
  26. package/dist/tui/prompts/spec-refinement-system.prompt.md +72 -0
  27. package/dist/tui/use-session-store-63YUGUFA.mjs +8 -0
  28. package/dist/tui/use-session-store-ACO2SMJC.mjs +8 -0
  29. package/dist/tui/use-session-store-BVFDAWOB.mjs +8 -0
  30. package/dist/tui/use-session-store-DJIZ3FQZ.mjs +9 -0
  31. package/dist/tui/use-session-store-EAIQA4UG.mjs +9 -0
  32. package/dist/tui/use-session-store-EFBAXC3G.mjs +8 -0
  33. package/dist/tui/use-session-store-FJOR4KTG.mjs +8 -0
  34. package/dist/tui/use-session-store-IJE5KVOC.mjs +8 -0
  35. package/dist/tui/use-session-store-KGAFXCKI.mjs +8 -0
  36. package/dist/tui/use-session-store-KS4DPNDY.mjs +8 -0
  37. package/dist/tui/use-session-store-MMHJENNL.mjs +8 -0
  38. package/dist/tui/use-session-store-OZ6HC4I2.mjs +9 -0
  39. package/dist/tui/use-session-store-PTMWISNJ.mjs +8 -0
  40. package/dist/tui/use-session-store-VCDECQMW.mjs +8 -0
  41. package/dist/tui/use-session-store-VOK5ML5J.mjs +9 -0
  42. package/package.json +33 -13
  43. package/dist/__tests__/export-cli.test.d.ts +0 -1
  44. package/dist/__tests__/export-cli.test.js +0 -70
  45. package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
  46. package/dist/__tests__/tui-args-policy.test.js +0 -50
  47. package/dist/acp-S2MHZOAD.mjs +0 -23
  48. package/dist/acp-UCCI44JY.mjs +0 -25
  49. package/dist/auth/credentials-store.d.ts +0 -2
  50. package/dist/auth/credentials-store.js +0 -5
  51. package/dist/auth/device-flow.d.ts +0 -36
  52. package/dist/auth/device-flow.js +0 -189
  53. package/dist/auth/jwt.d.ts +0 -1
  54. package/dist/auth/jwt.js +0 -6
  55. package/dist/auth/session.d.ts +0 -67
  56. package/dist/auth/session.js +0 -86
  57. package/dist/auth-login.d.ts +0 -34
  58. package/dist/auth-login.js +0 -202
  59. package/dist/auth-logout.d.ts +0 -25
  60. package/dist/auth-logout.js +0 -115
  61. package/dist/auth-status.d.ts +0 -24
  62. package/dist/auth-status.js +0 -109
  63. package/dist/backlog-generate.d.ts +0 -11
  64. package/dist/backlog-generate.js +0 -308
  65. package/dist/backlog-health.d.ts +0 -11
  66. package/dist/backlog-health.js +0 -287
  67. package/dist/bridge-login.d.ts +0 -40
  68. package/dist/bridge-login.js +0 -277
  69. package/dist/chunk-3PAYRI4G.mjs +0 -2428
  70. package/dist/chunk-M4CS3A25.mjs +0 -2426
  71. package/dist/commands/auth/login.d.ts +0 -30
  72. package/dist/commands/auth/login.js +0 -164
  73. package/dist/commands/auth/logout.d.ts +0 -25
  74. package/dist/commands/auth/logout.js +0 -115
  75. package/dist/commands/auth/status.d.ts +0 -24
  76. package/dist/commands/auth/status.js +0 -109
  77. package/dist/commands/backlog/generate.d.ts +0 -11
  78. package/dist/commands/backlog/generate.js +0 -308
  79. package/dist/commands/backlog/health.d.ts +0 -11
  80. package/dist/commands/backlog/health.js +0 -287
  81. package/dist/commands/bridge/login.d.ts +0 -36
  82. package/dist/commands/bridge/login.js +0 -258
  83. package/dist/commands/export.d.ts +0 -35
  84. package/dist/commands/export.js +0 -485
  85. package/dist/commands/marketplace-export.d.ts +0 -21
  86. package/dist/commands/marketplace-export.js +0 -214
  87. package/dist/commands/project-clean.d.ts +0 -1
  88. package/dist/commands/project-clean.js +0 -126
  89. package/dist/commands/repo/common.d.ts +0 -105
  90. package/dist/commands/repo/common.js +0 -775
  91. package/dist/commands/repo/detach.d.ts +0 -2
  92. package/dist/commands/repo/detach.js +0 -120
  93. package/dist/commands/repo/register.d.ts +0 -21
  94. package/dist/commands/repo/register.js +0 -175
  95. package/dist/commands/repo/sync.d.ts +0 -22
  96. package/dist/commands/repo/sync.js +0 -873
  97. package/dist/commands/skills-import-local.d.ts +0 -16
  98. package/dist/commands/skills-import-local.js +0 -352
  99. package/dist/commands/spec/drift-check.d.ts +0 -3
  100. package/dist/commands/spec/drift-check.js +0 -186
  101. package/dist/commands/spec/frontmatter.d.ts +0 -11
  102. package/dist/commands/spec/frontmatter.js +0 -219
  103. package/dist/commands/spec/lint.d.ts +0 -11
  104. package/dist/commands/spec/lint.js +0 -499
  105. package/dist/commands/spec/parse.d.ts +0 -11
  106. package/dist/commands/spec/parse.js +0 -162
  107. package/dist/export.d.ts +0 -35
  108. package/dist/export.js +0 -485
  109. package/dist/main.d.ts +0 -1
  110. package/dist/marketplace-export.d.ts +0 -21
  111. package/dist/marketplace-export.js +0 -214
  112. package/dist/project-clean.d.ts +0 -1
  113. package/dist/project-clean.js +0 -126
  114. package/dist/project-context.d.ts +0 -99
  115. package/dist/project-context.js +0 -376
  116. package/dist/repo-common.d.ts +0 -101
  117. package/dist/repo-common.js +0 -671
  118. package/dist/repo-detach.d.ts +0 -2
  119. package/dist/repo-detach.js +0 -102
  120. package/dist/repo-ingest.d.ts +0 -29
  121. package/dist/repo-ingest.js +0 -305
  122. package/dist/repo-register.d.ts +0 -21
  123. package/dist/repo-register.js +0 -175
  124. package/dist/repo-sync.d.ts +0 -16
  125. package/dist/repo-sync.js +0 -152
  126. package/dist/resources/prompt-loader.d.ts +0 -1
  127. package/dist/resources/prompt-loader.js +0 -62
  128. package/dist/skills-import-local.d.ts +0 -16
  129. package/dist/skills-import-local.js +0 -352
  130. package/dist/spec-drift-check.d.ts +0 -3
  131. package/dist/spec-drift-check.js +0 -186
  132. package/dist/spec-frontmatter.d.ts +0 -11
  133. package/dist/spec-frontmatter.js +0 -219
  134. package/dist/spec-lint.d.ts +0 -11
  135. package/dist/spec-lint.js +0 -499
  136. package/dist/spec-parse.d.ts +0 -11
  137. package/dist/spec-parse.js +0 -162
  138. package/dist/stubs/dotenv.d.ts +0 -5
  139. package/dist/stubs/dotenv.js +0 -6
  140. package/dist/stubs/typeorm.d.ts +0 -22
  141. package/dist/stubs/typeorm.js +0 -28
  142. package/dist/tui-bundle.d.ts +0 -1
  143. package/dist/tui-bundle.js +0 -5
  144. package/dist/tui-entry.mjs +0 -1407
  145. package/dist/utils/cli-runtime.d.ts +0 -5
  146. package/dist/utils/cli-runtime.js +0 -22
  147. package/dist/utils/help-error.d.ts +0 -7
  148. package/dist/utils/help-error.js +0 -14
  149. package/dist/utils/interaction.d.ts +0 -19
  150. package/dist/utils/interaction.js +0 -93
  151. package/dist/utils/structured-log.d.ts +0 -7
  152. package/dist/utils/structured-log.js +0 -112
  153. package/dist/utils/trpc-url.d.ts +0 -4
  154. package/dist/utils/trpc-url.js +0 -15
@@ -1,189 +0,0 @@
1
- "use strict";
2
- /**
3
- * RFC 8628 Device Authorization Grant (Device Flow)
4
- *
5
- * Implements the OAuth 2.0 Device Authorization Grant for CLI authentication
6
- * against a Keycloak realm.
7
- *
8
- * @see https://www.rfc-editor.org/rfc/rfc8628
9
- */
10
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- var desc = Object.getOwnPropertyDescriptor(m, k);
13
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
- desc = { enumerable: true, get: function() { return m[k]; } };
15
- }
16
- Object.defineProperty(o, k2, desc);
17
- }) : (function(o, m, k, k2) {
18
- if (k2 === undefined) k2 = k;
19
- o[k2] = m[k];
20
- }));
21
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
- Object.defineProperty(o, "default", { enumerable: true, value: v });
23
- }) : function(o, v) {
24
- o["default"] = v;
25
- });
26
- var __importStar = (this && this.__importStar) || (function () {
27
- var ownKeys = function(o) {
28
- ownKeys = Object.getOwnPropertyNames || function (o) {
29
- var ar = [];
30
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
- return ar;
32
- };
33
- return ownKeys(o);
34
- };
35
- return function (mod) {
36
- if (mod && mod.__esModule) return mod;
37
- var result = {};
38
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
- __setModuleDefault(result, mod);
40
- return result;
41
- };
42
- })();
43
- Object.defineProperty(exports, "__esModule", { value: true });
44
- exports.defaultOpenBrowser = void 0;
45
- exports.performDeviceFlow = performDeviceFlow;
46
- const zod_1 = require("zod");
47
- const DeviceAuthorizationResponseSchema = zod_1.z.object({
48
- device_code: zod_1.z.string(),
49
- user_code: zod_1.z.string(),
50
- verification_uri: zod_1.z.string(),
51
- verification_uri_complete: zod_1.z.string(),
52
- expires_in: zod_1.z.number(),
53
- interval: zod_1.z.number(),
54
- });
55
- const TokenSuccessResponseSchema = zod_1.z.object({
56
- access_token: zod_1.z.string(),
57
- refresh_token: zod_1.z.string(),
58
- expires_in: zod_1.z.number(),
59
- id_token: zod_1.z.string().optional(),
60
- token_type: zod_1.z.string(),
61
- });
62
- const TokenErrorResponseSchema = zod_1.z.object({
63
- error: zod_1.z.string(),
64
- error_description: zod_1.z.string().optional(),
65
- });
66
- /** Default browser opener — dynamically imports the `open` package. */
67
- const defaultOpenBrowser = async (url) => {
68
- const { default: open } = await Promise.resolve().then(() => __importStar(require("open")));
69
- await open(url);
70
- };
71
- exports.defaultOpenBrowser = defaultOpenBrowser;
72
- const defaultDeps = {
73
- stdout: process.stdout,
74
- stderr: process.stderr,
75
- openBrowser: defaultOpenBrowser,
76
- };
77
- /** Resolves deps, filling in defaults for any omitted fields. */
78
- function resolveDeps(deps) {
79
- return {
80
- stdout: deps?.stdout ?? defaultDeps.stdout,
81
- stderr: deps?.stderr ?? defaultDeps.stderr,
82
- openBrowser: deps?.openBrowser ?? defaultDeps.openBrowser,
83
- };
84
- }
85
- /** Pause for `ms` milliseconds. */
86
- function sleep(ms) {
87
- return new Promise((resolve) => setTimeout(resolve, ms));
88
- }
89
- /**
90
- * Perform the RFC 8628 Device Authorization Grant against a Keycloak realm.
91
- *
92
- * @param keycloakUrl - Base Keycloak URL, e.g. `https://auth.example.com`
93
- * @param realm - Keycloak realm name
94
- * @param clientId - OAuth2 client ID (must be a public client)
95
- * @param deps - Optional dependency overrides for testing / custom I/O
96
- * @returns Resolved tokens once the user authorises the device
97
- */
98
- async function performDeviceFlow(keycloakUrl, realm, clientId, deps) {
99
- const { stdout, stderr, openBrowser } = resolveDeps(deps);
100
- // -------------------------------------------------------------------------
101
- // Step 1: Request device code
102
- // -------------------------------------------------------------------------
103
- const deviceAuthUrl = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/auth/device`;
104
- const deviceAuthBody = new URLSearchParams({
105
- client_id: clientId,
106
- scope: "openid email profile offline_access spekn-api",
107
- });
108
- const deviceAuthResponse = await fetch(deviceAuthUrl, {
109
- method: "POST",
110
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
111
- body: deviceAuthBody.toString(),
112
- });
113
- if (!deviceAuthResponse.ok) {
114
- const text = await deviceAuthResponse.text();
115
- throw new Error(`Device authorization request failed (${deviceAuthResponse.status}): ${text}`);
116
- }
117
- const deviceAuth = DeviceAuthorizationResponseSchema.parse(await deviceAuthResponse.json());
118
- const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval, } = deviceAuth;
119
- // -------------------------------------------------------------------------
120
- // Step 2: Display instructions to the user
121
- // -------------------------------------------------------------------------
122
- stdout.write(`\nTo sign in, open this URL in your browser:\n` +
123
- ` ${verification_uri_complete}\n\n` +
124
- `Enter code: ${user_code}\n\n` +
125
- `Waiting for authorization...\n`);
126
- // -------------------------------------------------------------------------
127
- // Step 3: Open browser (best-effort)
128
- // -------------------------------------------------------------------------
129
- try {
130
- await openBrowser(verification_uri_complete);
131
- }
132
- catch (err) {
133
- // Silently ignore browser-open failures; user still has the URL above.
134
- const message = err instanceof Error ? err.message : String(err);
135
- stderr.write(`(Could not open browser automatically: ${message})\n`);
136
- }
137
- // -------------------------------------------------------------------------
138
- // Step 4: Poll token endpoint
139
- // -------------------------------------------------------------------------
140
- const tokenUrl = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`;
141
- const deadline = Date.now() + expires_in * 1000;
142
- // RFC 8628 §3.5: default interval is 5 seconds when not specified.
143
- let pollInterval = (interval ?? 5) * 1000;
144
- while (Date.now() < deadline) {
145
- await sleep(pollInterval);
146
- const tokenBody = new URLSearchParams({
147
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
148
- device_code,
149
- client_id: clientId,
150
- });
151
- const tokenResponse = await fetch(tokenUrl, {
152
- method: "POST",
153
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
154
- body: tokenBody.toString(),
155
- });
156
- if (tokenResponse.ok) {
157
- // Success — return tokens.
158
- const token = TokenSuccessResponseSchema.parse(await tokenResponse.json());
159
- return {
160
- accessToken: token.access_token,
161
- refreshToken: token.refresh_token,
162
- expiresIn: token.expires_in,
163
- idToken: token.id_token,
164
- };
165
- }
166
- // Non-200 responses carry an RFC 8628 error code in the JSON body.
167
- const errorBody = TokenErrorResponseSchema.parse(await tokenResponse.json());
168
- const errorCode = errorBody.error;
169
- switch (errorCode) {
170
- case "authorization_pending":
171
- // User has not yet authorised — keep polling.
172
- break;
173
- case "slow_down":
174
- // RFC 8628 §3.5: increase interval by 5 seconds and continue.
175
- pollInterval += 5000;
176
- break;
177
- case "expired_token":
178
- throw new Error("Device code expired. Please try again.");
179
- case "access_denied":
180
- throw new Error("Authorization denied by user.");
181
- default:
182
- throw new Error(`Unexpected token error "${errorCode}": ${errorBody.error_description ?? "no description"}`);
183
- }
184
- }
185
- // -------------------------------------------------------------------------
186
- // Step 5: Timeout — device code has expired without user action.
187
- // -------------------------------------------------------------------------
188
- throw new Error(`Authorization timed out after ${expires_in} seconds. Please try again.`);
189
- }
@@ -1 +0,0 @@
1
- export { decodeJwtPayload, extractUserIdentity } from "@spekn/shared";
package/dist/auth/jwt.js DELETED
@@ -1,6 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractUserIdentity = exports.decodeJwtPayload = void 0;
4
- var shared_1 = require("@spekn/shared");
5
- Object.defineProperty(exports, "decodeJwtPayload", { enumerable: true, get: function () { return shared_1.decodeJwtPayload; } });
6
- Object.defineProperty(exports, "extractUserIdentity", { enumerable: true, get: function () { return shared_1.extractUserIdentity; } });
@@ -1,67 +0,0 @@
1
- import type { CliCredentials } from "./credentials-store.js";
2
- import type { CredentialsStore } from "./credentials-store.js";
3
- import type { DeviceFlowDeps, DeviceFlowResult } from "./device-flow.js";
4
- interface BuildCredentialsInput {
5
- result: DeviceFlowResult;
6
- keycloakUrl: string;
7
- realm: string;
8
- organizationId?: string;
9
- }
10
- interface BuildCredentialsOutput {
11
- credentials: CliCredentials;
12
- user: {
13
- sub: string;
14
- email: string;
15
- name?: string;
16
- };
17
- }
18
- /**
19
- * Build persisted CLI credentials from a successful Keycloak device-flow result.
20
- */
21
- export declare function buildCredentialsFromDeviceFlow(input: BuildCredentialsInput): BuildCredentialsOutput;
22
- export interface EnsureAuthenticatedInput {
23
- keycloakUrl: string;
24
- realm: string;
25
- credentialsStore: CredentialsStore;
26
- performDeviceFlow: (keycloakUrl: string, realm: string, clientId: string, deps?: Partial<DeviceFlowDeps>) => Promise<DeviceFlowResult>;
27
- stdout: {
28
- write(s: string): void;
29
- };
30
- stderr: {
31
- write(s: string): void;
32
- };
33
- forceDeviceFlow?: boolean;
34
- }
35
- export interface EnsureAuthenticatedResult {
36
- accessToken: string;
37
- credentials: CliCredentials;
38
- user: {
39
- sub: string;
40
- email: string;
41
- name?: string;
42
- };
43
- source: "existing" | "device_flow";
44
- }
45
- export interface ResolveOrganizationIdInput {
46
- existingOrganizationId?: string;
47
- envOrganizationId?: string;
48
- fetchOrganizations: () => Promise<Array<{
49
- id: string;
50
- name: string;
51
- }>>;
52
- stdout?: {
53
- write(s: string): void;
54
- };
55
- }
56
- /**
57
- * Ensure CLI authentication is available.
58
- * - Reuses existing valid token by default.
59
- * - Runs device flow when forced or when no valid token exists.
60
- */
61
- export declare function ensureAuthenticated(input: EnsureAuthenticatedInput): Promise<EnsureAuthenticatedResult>;
62
- /**
63
- * Resolve effective organization context:
64
- * stored credentials first, then env, then API discovery fallback.
65
- */
66
- export declare function resolveOrganizationId(input: ResolveOrganizationIdInput): Promise<string | undefined>;
67
- export {};
@@ -1,86 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildCredentialsFromDeviceFlow = buildCredentialsFromDeviceFlow;
4
- exports.ensureAuthenticated = ensureAuthenticated;
5
- exports.resolveOrganizationId = resolveOrganizationId;
6
- const jwt_js_1 = require("./jwt.js");
7
- /**
8
- * Build persisted CLI credentials from a successful Keycloak device-flow result.
9
- */
10
- function buildCredentialsFromDeviceFlow(input) {
11
- const claims = (0, jwt_js_1.decodeJwtPayload)(input.result.accessToken);
12
- const user = (0, jwt_js_1.extractUserIdentity)(claims);
13
- const credentials = {
14
- accessToken: input.result.accessToken,
15
- refreshToken: input.result.refreshToken,
16
- expiresAt: Date.now() + input.result.expiresIn * 1000,
17
- keycloakUrl: input.keycloakUrl,
18
- realm: input.realm,
19
- organizationId: input.organizationId,
20
- user,
21
- };
22
- return { credentials, user };
23
- }
24
- /**
25
- * Ensure CLI authentication is available.
26
- * - Reuses existing valid token by default.
27
- * - Runs device flow when forced or when no valid token exists.
28
- */
29
- async function ensureAuthenticated(input) {
30
- if (!input.forceDeviceFlow) {
31
- const existingToken = await input.credentialsStore.getValidToken();
32
- const existingCreds = input.credentialsStore.load();
33
- if (existingToken && existingCreds) {
34
- return {
35
- accessToken: existingToken,
36
- credentials: existingCreds,
37
- user: existingCreds.user ?? {
38
- sub: "unknown",
39
- email: "unknown",
40
- },
41
- source: "existing",
42
- };
43
- }
44
- }
45
- const result = await input.performDeviceFlow(input.keycloakUrl, input.realm, "spekn-cli", { stdout: input.stdout, stderr: input.stderr });
46
- const { credentials, user } = buildCredentialsFromDeviceFlow({
47
- result,
48
- keycloakUrl: input.keycloakUrl,
49
- realm: input.realm,
50
- });
51
- input.credentialsStore.save(credentials);
52
- return {
53
- accessToken: result.accessToken,
54
- credentials,
55
- user,
56
- source: "device_flow",
57
- };
58
- }
59
- /**
60
- * Resolve effective organization context:
61
- * stored credentials first, then env, then API discovery fallback.
62
- */
63
- async function resolveOrganizationId(input) {
64
- if (input.existingOrganizationId) {
65
- return input.existingOrganizationId;
66
- }
67
- if (input.envOrganizationId) {
68
- return input.envOrganizationId;
69
- }
70
- try {
71
- const orgs = await input.fetchOrganizations();
72
- if (orgs.length === 0)
73
- return undefined;
74
- if (orgs.length > 1 && input.stdout) {
75
- input.stdout.write(`\nYou belong to ${orgs.length} organizations:\n`);
76
- for (const org of orgs) {
77
- input.stdout.write(` - ${org.name} (${org.id})\n`);
78
- }
79
- input.stdout.write(`\nAuto-selected first organization. Use SPEKN_ORGANIZATION_ID to override.\n`);
80
- }
81
- return orgs[0]?.id;
82
- }
83
- catch {
84
- return undefined;
85
- }
86
- }
@@ -1,34 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * auth login CLI command
4
- *
5
- * Authenticates the CLI user via the Keycloak Device Authorization Grant
6
- * (RFC 8628) and persists credentials to ~/.spekn/credentials.json.
7
- *
8
- * Usage: spekn auth login [--keycloak-url <url>] [--realm <realm>]
9
- */
10
- import type { DeviceFlowDeps, DeviceFlowResult } from "./auth/device-flow.js";
11
- import { CredentialsStore } from "./auth/credentials-store.js";
12
- interface CLIOptions {
13
- keycloakUrl: string;
14
- realm: string;
15
- }
16
- interface Deps {
17
- stdout: {
18
- write(s: string): void;
19
- };
20
- stderr: {
21
- write(s: string): void;
22
- };
23
- performDeviceFlow: (keycloakUrl: string, realm: string, clientId: string, deps?: Partial<DeviceFlowDeps>) => Promise<DeviceFlowResult>;
24
- credentialsStore: CredentialsStore;
25
- }
26
- declare function parseArgs(args: string[]): CLIOptions;
27
- /**
28
- * Decode the payload of a JWT access token without verification.
29
- * Returns the raw claims object, or null if decoding fails.
30
- */
31
- declare function decodeJwtPayload(token: string): Record<string, unknown> | null;
32
- export declare function runAuthLoginCli(args: string[], deps?: Partial<Deps>): Promise<number>;
33
- declare function main(): Promise<void>;
34
- export { main, parseArgs, decodeJwtPayload };
@@ -1,202 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * auth login CLI command
5
- *
6
- * Authenticates the CLI user via the Keycloak Device Authorization Grant
7
- * (RFC 8628) and persists credentials to ~/.spekn/credentials.json.
8
- *
9
- * Usage: spekn auth login [--keycloak-url <url>] [--realm <realm>]
10
- */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.runAuthLoginCli = runAuthLoginCli;
13
- exports.main = main;
14
- exports.parseArgs = parseArgs;
15
- exports.decodeJwtPayload = decodeJwtPayload;
16
- const client_1 = require("@trpc/client");
17
- const device_flow_js_1 = require("./auth/device-flow.js");
18
- const credentials_store_js_1 = require("./auth/credentials-store.js");
19
- const help_error_1 = require("./utils/help-error");
20
- const trpc_url_1 = require("./utils/trpc-url");
21
- const structured_log_1 = require("./utils/structured-log");
22
- const defaultDeps = {
23
- stdout: process.stdout,
24
- stderr: process.stderr,
25
- performDeviceFlow: device_flow_js_1.performDeviceFlow,
26
- credentialsStore: new credentials_store_js_1.CredentialsStore(),
27
- };
28
- function resolveDeps(deps) {
29
- return {
30
- stdout: deps?.stdout ?? defaultDeps.stdout,
31
- stderr: deps?.stderr ?? defaultDeps.stderr,
32
- performDeviceFlow: deps?.performDeviceFlow ?? defaultDeps.performDeviceFlow,
33
- credentialsStore: deps?.credentialsStore ?? defaultDeps.credentialsStore,
34
- };
35
- }
36
- function printHelp(stderr) {
37
- stderr.write(`
38
- auth login - Authenticate the Spekn CLI via browser-based device flow
39
-
40
- USAGE:
41
- spekn auth login [options]
42
-
43
- OPTIONS:
44
- --keycloak-url <url> Keycloak base URL (default: KEYCLOAK_URL or https://auth.spekn.com)
45
- --realm <realm> Keycloak realm name (default: KEYCLOAK_REALM or spekn)
46
- --help Show this help message
47
-
48
- ENVIRONMENT:
49
- KEYCLOAK_URL Keycloak base URL
50
- KEYCLOAK_REALM Keycloak realm name
51
-
52
- EXAMPLES:
53
- spekn auth login
54
- spekn auth login --keycloak-url https://auth.example.com --realm my-realm
55
- `);
56
- }
57
- function parseArgs(args) {
58
- const keycloakUrlDefault = process.env["KEYCLOAK_URL"] ?? "https://auth.spekn.com";
59
- const realmDefault = process.env["KEYCLOAK_REALM"] ?? "spekn";
60
- let keycloakUrl = keycloakUrlDefault;
61
- let realm = realmDefault;
62
- for (let index = 0; index < args.length; index++) {
63
- const arg = args[index];
64
- if (arg === "--help" || arg === "-h") {
65
- throw new help_error_1.HelpRequestedError();
66
- }
67
- if (arg === "--keycloak-url" && args[index + 1]) {
68
- keycloakUrl = args[++index];
69
- continue;
70
- }
71
- if (arg.startsWith("--keycloak-url=")) {
72
- keycloakUrl = arg.slice("--keycloak-url=".length);
73
- continue;
74
- }
75
- if (arg === "--realm" && args[index + 1]) {
76
- realm = args[++index];
77
- continue;
78
- }
79
- if (arg.startsWith("--realm=")) {
80
- realm = arg.slice("--realm=".length);
81
- continue;
82
- }
83
- }
84
- return { keycloakUrl, realm };
85
- }
86
- /**
87
- * Decode the payload of a JWT access token without verification.
88
- * Returns the raw claims object, or null if decoding fails.
89
- */
90
- function decodeJwtPayload(token) {
91
- try {
92
- const parts = token.split(".");
93
- if (parts.length !== 3) {
94
- return null;
95
- }
96
- // Base64url → standard base64
97
- const base64 = parts[1]
98
- .replace(/-/g, "+")
99
- .replace(/_/g, "/");
100
- // Pad to a multiple of 4
101
- const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
102
- const json = Buffer.from(padded, "base64").toString("utf-8");
103
- return JSON.parse(json);
104
- }
105
- catch {
106
- return null;
107
- }
108
- }
109
- async function runAuthLoginCli(args, deps) {
110
- const resolved = resolveDeps(deps);
111
- try {
112
- const options = parseArgs(args);
113
- (0, structured_log_1.appendCliStructuredLog)({
114
- source: "cli.auth.login",
115
- level: "info",
116
- message: "Starting auth login",
117
- details: { keycloakUrl: options.keycloakUrl, realm: options.realm },
118
- });
119
- const result = await resolved.performDeviceFlow(options.keycloakUrl, options.realm, "spekn-cli", { stdout: resolved.stdout, stderr: resolved.stderr });
120
- // Decode the JWT payload to extract user identity fields.
121
- const claims = decodeJwtPayload(result.accessToken);
122
- const sub = typeof claims?.["sub"] === "string" ? claims["sub"] : "unknown";
123
- const email = typeof claims?.["email"] === "string" ? claims["email"] : "unknown";
124
- const name = typeof claims?.["name"] === "string"
125
- ? claims["name"]
126
- : typeof claims?.["preferred_username"] === "string"
127
- ? claims["preferred_username"]
128
- : undefined;
129
- // Fetch the user's organizations from the API to resolve organizationId
130
- const apiUrl = process.env.SPEKN_API_URL ?? "https://app.spekn.com";
131
- const client = (0, client_1.createTRPCProxyClient)({
132
- links: [
133
- (0, client_1.httpBatchLink)({
134
- url: (0, trpc_url_1.normalizeTrpcUrl)(apiUrl),
135
- headers: {
136
- authorization: `Bearer ${result.accessToken}`,
137
- },
138
- }),
139
- ],
140
- });
141
- let organizationId;
142
- try {
143
- const orgs = await client.organization.list.query();
144
- if (orgs.length === 1) {
145
- organizationId = orgs[0].id;
146
- }
147
- else if (orgs.length > 1) {
148
- resolved.stdout.write(`\nYou belong to ${orgs.length} organizations:\n`);
149
- for (const org of orgs) {
150
- resolved.stdout.write(` - ${org.name} (${org.id})\n`);
151
- }
152
- resolved.stdout.write(`\nAuto-selected first organization. Use SPEKN_ORGANIZATION_ID to override.\n`);
153
- organizationId = orgs[0].id;
154
- }
155
- }
156
- catch {
157
- // Non-fatal — user can set SPEKN_ORGANIZATION_ID manually
158
- }
159
- const credentials = {
160
- accessToken: result.accessToken,
161
- refreshToken: result.refreshToken,
162
- expiresAt: Date.now() + result.expiresIn * 1000,
163
- keycloakUrl: options.keycloakUrl,
164
- realm: options.realm,
165
- organizationId,
166
- user: { sub, email, name },
167
- };
168
- resolved.credentialsStore.save(credentials);
169
- resolved.stdout.write(`Logged in as ${email}\n`);
170
- if (organizationId) {
171
- resolved.stdout.write(`Organization: ${organizationId}\n`);
172
- }
173
- (0, structured_log_1.appendCliStructuredLog)({
174
- source: "cli.auth.login",
175
- level: "info",
176
- message: "Auth login completed",
177
- details: { email, organizationId },
178
- });
179
- return 0;
180
- }
181
- catch (error) {
182
- if (error instanceof help_error_1.HelpRequestedError) {
183
- printHelp(resolved.stderr);
184
- return 0;
185
- }
186
- const message = error instanceof Error ? error.message : String(error);
187
- resolved.stderr.write(`Error: ${message}\n`);
188
- (0, structured_log_1.appendCliStructuredLog)({
189
- source: "cli.auth.login",
190
- level: "error",
191
- message,
192
- });
193
- return 1;
194
- }
195
- }
196
- async function main() {
197
- const exitCode = await runAuthLoginCli(process.argv.slice(2));
198
- process.exit(exitCode);
199
- }
200
- if (require.main === module) {
201
- void main();
202
- }
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * auth logout CLI command
4
- *
5
- * Revokes the stored refresh token against Keycloak (best-effort) and then
6
- * removes the local credentials file at ~/.spekn/credentials.json.
7
- *
8
- * Usage: spekn auth logout
9
- */
10
- import { CredentialsStore } from "./auth/credentials-store.js";
11
- interface Deps {
12
- stdout: {
13
- write(s: string): void;
14
- };
15
- stderr: {
16
- write(s: string): void;
17
- };
18
- credentialsStore: CredentialsStore;
19
- /** POST to the Keycloak logout endpoint. Injected for testing. */
20
- revokeToken: (logoutUrl: string, body: URLSearchParams) => Promise<void>;
21
- }
22
- declare function parseArgs(args: string[]): void;
23
- export declare function runAuthLogoutCli(args: string[], deps?: Partial<Deps>): Promise<number>;
24
- declare function main(): Promise<void>;
25
- export { main, parseArgs };