@spekn/cli 1.0.0 → 1.0.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.
Files changed (154) hide show
  1. package/README.md +58 -0
  2. package/dist/main.js +3707 -611
  3. package/dist/tui/index.mjs +2 -2
  4. package/package.json +29 -12
  5. package/dist/__tests__/export-cli.test.d.ts +0 -1
  6. package/dist/__tests__/export-cli.test.js +0 -70
  7. package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
  8. package/dist/__tests__/tui-args-policy.test.js +0 -50
  9. package/dist/acp-S2MHZOAD.mjs +0 -23
  10. package/dist/acp-UCCI44JY.mjs +0 -25
  11. package/dist/auth/credentials-store.d.ts +0 -2
  12. package/dist/auth/credentials-store.js +0 -5
  13. package/dist/auth/device-flow.d.ts +0 -36
  14. package/dist/auth/device-flow.js +0 -189
  15. package/dist/auth/jwt.d.ts +0 -1
  16. package/dist/auth/jwt.js +0 -6
  17. package/dist/auth/session.d.ts +0 -67
  18. package/dist/auth/session.js +0 -86
  19. package/dist/auth-login.d.ts +0 -34
  20. package/dist/auth-login.js +0 -202
  21. package/dist/auth-logout.d.ts +0 -25
  22. package/dist/auth-logout.js +0 -115
  23. package/dist/auth-status.d.ts +0 -24
  24. package/dist/auth-status.js +0 -109
  25. package/dist/backlog-generate.d.ts +0 -11
  26. package/dist/backlog-generate.js +0 -308
  27. package/dist/backlog-health.d.ts +0 -11
  28. package/dist/backlog-health.js +0 -287
  29. package/dist/bridge-login.d.ts +0 -40
  30. package/dist/bridge-login.js +0 -277
  31. package/dist/chunk-3PAYRI4G.mjs +0 -2428
  32. package/dist/chunk-M4CS3A25.mjs +0 -2426
  33. package/dist/commands/auth/login.d.ts +0 -30
  34. package/dist/commands/auth/login.js +0 -164
  35. package/dist/commands/auth/logout.d.ts +0 -25
  36. package/dist/commands/auth/logout.js +0 -115
  37. package/dist/commands/auth/status.d.ts +0 -24
  38. package/dist/commands/auth/status.js +0 -109
  39. package/dist/commands/backlog/generate.d.ts +0 -11
  40. package/dist/commands/backlog/generate.js +0 -308
  41. package/dist/commands/backlog/health.d.ts +0 -11
  42. package/dist/commands/backlog/health.js +0 -287
  43. package/dist/commands/bridge/login.d.ts +0 -36
  44. package/dist/commands/bridge/login.js +0 -258
  45. package/dist/commands/export.d.ts +0 -35
  46. package/dist/commands/export.js +0 -485
  47. package/dist/commands/marketplace-export.d.ts +0 -21
  48. package/dist/commands/marketplace-export.js +0 -214
  49. package/dist/commands/project-clean.d.ts +0 -1
  50. package/dist/commands/project-clean.js +0 -126
  51. package/dist/commands/repo/common.d.ts +0 -105
  52. package/dist/commands/repo/common.js +0 -775
  53. package/dist/commands/repo/detach.d.ts +0 -2
  54. package/dist/commands/repo/detach.js +0 -120
  55. package/dist/commands/repo/register.d.ts +0 -21
  56. package/dist/commands/repo/register.js +0 -175
  57. package/dist/commands/repo/sync.d.ts +0 -22
  58. package/dist/commands/repo/sync.js +0 -873
  59. package/dist/commands/skills-import-local.d.ts +0 -16
  60. package/dist/commands/skills-import-local.js +0 -352
  61. package/dist/commands/spec/drift-check.d.ts +0 -3
  62. package/dist/commands/spec/drift-check.js +0 -186
  63. package/dist/commands/spec/frontmatter.d.ts +0 -11
  64. package/dist/commands/spec/frontmatter.js +0 -219
  65. package/dist/commands/spec/lint.d.ts +0 -11
  66. package/dist/commands/spec/lint.js +0 -499
  67. package/dist/commands/spec/parse.d.ts +0 -11
  68. package/dist/commands/spec/parse.js +0 -162
  69. package/dist/export.d.ts +0 -35
  70. package/dist/export.js +0 -485
  71. package/dist/main.d.ts +0 -1
  72. package/dist/marketplace-export.d.ts +0 -21
  73. package/dist/marketplace-export.js +0 -214
  74. package/dist/project-clean.d.ts +0 -1
  75. package/dist/project-clean.js +0 -126
  76. package/dist/project-context.d.ts +0 -99
  77. package/dist/project-context.js +0 -376
  78. package/dist/repo-common.d.ts +0 -101
  79. package/dist/repo-common.js +0 -671
  80. package/dist/repo-detach.d.ts +0 -2
  81. package/dist/repo-detach.js +0 -102
  82. package/dist/repo-ingest.d.ts +0 -29
  83. package/dist/repo-ingest.js +0 -305
  84. package/dist/repo-register.d.ts +0 -21
  85. package/dist/repo-register.js +0 -175
  86. package/dist/repo-sync.d.ts +0 -16
  87. package/dist/repo-sync.js +0 -152
  88. package/dist/resources/prompt-loader.d.ts +0 -1
  89. package/dist/resources/prompt-loader.js +0 -62
  90. package/dist/skills-import-local.d.ts +0 -16
  91. package/dist/skills-import-local.js +0 -352
  92. package/dist/spec-drift-check.d.ts +0 -3
  93. package/dist/spec-drift-check.js +0 -186
  94. package/dist/spec-frontmatter.d.ts +0 -11
  95. package/dist/spec-frontmatter.js +0 -219
  96. package/dist/spec-lint.d.ts +0 -11
  97. package/dist/spec-lint.js +0 -499
  98. package/dist/spec-parse.d.ts +0 -11
  99. package/dist/spec-parse.js +0 -162
  100. package/dist/stubs/dotenv.d.ts +0 -5
  101. package/dist/stubs/dotenv.js +0 -6
  102. package/dist/stubs/typeorm.d.ts +0 -22
  103. package/dist/stubs/typeorm.js +0 -28
  104. package/dist/tui/app.d.ts +0 -7
  105. package/dist/tui/app.js +0 -122
  106. package/dist/tui/args.d.ts +0 -8
  107. package/dist/tui/args.js +0 -57
  108. package/dist/tui/capabilities/policy.d.ts +0 -7
  109. package/dist/tui/capabilities/policy.js +0 -64
  110. package/dist/tui/components/frame.d.ts +0 -8
  111. package/dist/tui/components/frame.js +0 -8
  112. package/dist/tui/components/status-bar.d.ts +0 -8
  113. package/dist/tui/components/status-bar.js +0 -8
  114. package/dist/tui/index.d.ts +0 -2
  115. package/dist/tui/index.js +0 -23
  116. package/dist/tui/keymap/use-global-keymap.d.ts +0 -19
  117. package/dist/tui/keymap/use-global-keymap.js +0 -82
  118. package/dist/tui/navigation/nav-items.d.ts +0 -3
  119. package/dist/tui/navigation/nav-items.js +0 -18
  120. package/dist/tui/screens/bridge.d.ts +0 -8
  121. package/dist/tui/screens/bridge.js +0 -19
  122. package/dist/tui/screens/decisions.d.ts +0 -5
  123. package/dist/tui/screens/decisions.js +0 -28
  124. package/dist/tui/screens/export.d.ts +0 -5
  125. package/dist/tui/screens/export.js +0 -16
  126. package/dist/tui/screens/home.d.ts +0 -5
  127. package/dist/tui/screens/home.js +0 -33
  128. package/dist/tui/screens/locked.d.ts +0 -5
  129. package/dist/tui/screens/locked.js +0 -9
  130. package/dist/tui/screens/specs.d.ts +0 -5
  131. package/dist/tui/screens/specs.js +0 -31
  132. package/dist/tui/services/client.d.ts +0 -1
  133. package/dist/tui/services/client.js +0 -18
  134. package/dist/tui/services/context-service.d.ts +0 -19
  135. package/dist/tui/services/context-service.js +0 -246
  136. package/dist/tui/shared-enums.d.ts +0 -16
  137. package/dist/tui/shared-enums.js +0 -19
  138. package/dist/tui/state/use-app-state.d.ts +0 -35
  139. package/dist/tui/state/use-app-state.js +0 -177
  140. package/dist/tui/types.d.ts +0 -77
  141. package/dist/tui/types.js +0 -2
  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
@@ -6760,10 +6760,10 @@ import { UnorderedList } from "@inkjs/ui";
6760
6760
 
6761
6761
  // src/utils/build-info.ts
6762
6762
  var BUILD_INFO = {
6763
- version: "1.0.0",
6763
+ version: "1.0.1",
6764
6764
  gitCommit: "c55e496",
6765
6765
  gitBranch: "main",
6766
- buildTime: "2026-02-27T09:52:46.694Z",
6766
+ buildTime: "2026-02-27T10:00:39.334Z",
6767
6767
  nodeVersion: process.version
6768
6768
  };
6769
6769
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spekn/cli",
3
- "version": "1.0.0",
4
- "description": "Spekn CLI — Spec-Driven Development toolchain",
3
+ "version": "1.0.1",
4
+ "description": "Spekn CLI — Spec-Driven Development toolchain. Installs the `spekn` command.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -9,8 +9,16 @@
9
9
  "spekn": "dist/main.js"
10
10
  },
11
11
  "files": [
12
- "dist"
12
+ "dist/main.js",
13
+ "dist/index.js",
14
+ "dist/index.d.ts",
15
+ "dist/tui/index.mjs",
16
+ "dist/resources/prompts/",
17
+ "README.md"
13
18
  ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
14
22
  "engines": {
15
23
  "node": ">=22",
16
24
  "npm": ">=10"
@@ -20,40 +28,49 @@
20
28
  "url": "https://github.com/spekn/spekn.git",
21
29
  "directory": "packages/cli"
22
30
  },
31
+ "homepage": "https://spekn.dev",
32
+ "bugs": {
33
+ "url": "https://github.com/spekn/spekn/issues"
34
+ },
23
35
  "keywords": [
24
36
  "spekn",
25
37
  "sdd",
26
38
  "spec-driven-development",
27
39
  "cli",
28
40
  "specifications",
29
- "ai-agents"
41
+ "ai-agents",
42
+ "governance",
43
+ "context-engineering"
30
44
  ],
31
45
  "scripts": {
32
46
  "build": "npm -w @spekn/tui run build && tsc --build && tsup && mkdir -p dist/tui && cp ../tui/dist/index.mjs dist/tui/index.mjs && mkdir -p dist/resources/prompts && cp -R src/resources/prompts/. dist/resources/prompts/",
33
47
  "dev": "tsc --build --watch",
34
48
  "clean": "rm -rf dist",
49
+ "prepublishOnly": "npm run build",
35
50
  "project-delete": "ts-node src/commands/project-clean.ts"
36
51
  },
37
52
  "dependencies": {
38
- "@inkjs/ui": "^2.0.0",
39
- "@trpc/client": "^10.45.4",
40
53
  "commander": "^14.0.3",
41
- "ink": "^6.8.0",
42
54
  "js-yaml": "^4.1.0",
43
55
  "open": "^10.2.0",
44
- "pg": "^8.18.0",
45
- "react": "^19.2.0",
46
- "typeorm": "^0.3.28",
47
- "zustand": "^5.0.8",
48
56
  "zod": "^4.3.6"
49
57
  },
58
+ "optionalDependencies": {
59
+ "pg": "^8.18.0",
60
+ "typeorm": "^0.3.28"
61
+ },
50
62
  "devDependencies": {
63
+ "@inkjs/ui": "^2.0.0",
51
64
  "@spekn/agents": "*",
52
65
  "@spekn/bridge": "*",
53
66
  "@spekn/check": "*",
54
67
  "@spekn/shared": "*",
55
68
  "@spekn/tui": "*",
69
+ "@trpc/client": "^10.45.4",
70
+ "ink": "^6.8.0",
71
+ "react": "^19.2.0",
56
72
  "tsup": "^8.5.1",
57
- "typescript": "^5.9.3"
73
+ "typescript": "^5.9.3",
74
+ "zustand": "^5.0.8"
58
75
  }
59
76
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,70 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strict_1 = __importDefault(require("node:assert/strict"));
7
- const export_1 = require("../export");
8
- function createDeps() {
9
- const state = {
10
- stdout: '',
11
- stderr: '',
12
- writes: [],
13
- };
14
- const deps = {
15
- createClient: () => ({
16
- export: {
17
- generate: {
18
- mutate: async () => ({
19
- content: 'GENERATED CONTENT\n',
20
- anchorCount: 2,
21
- }),
22
- },
23
- },
24
- }),
25
- writeFile: (filePath, content) => {
26
- state.writes.push({ path: filePath, content });
27
- },
28
- stdout: (content) => {
29
- state.stdout += content;
30
- },
31
- stderr: (content) => {
32
- state.stderr += content;
33
- },
34
- };
35
- return { state, deps };
36
- }
37
- async function testStdoutMode() {
38
- const { state, deps } = createDeps();
39
- const code = await (0, export_1.runExportCli)(['--project', '11111111-1111-4111-8111-111111111111', '--format', 'claude-md'], deps);
40
- strict_1.default.equal(code, 0);
41
- strict_1.default.equal(state.stdout, 'GENERATED CONTENT\n');
42
- strict_1.default.equal(state.writes.length, 0);
43
- }
44
- async function testOutputMode() {
45
- const { state, deps } = createDeps();
46
- const code = await (0, export_1.runExportCli)([
47
- '--project',
48
- '11111111-1111-4111-8111-111111111111',
49
- '--format',
50
- 'cursorrules',
51
- '--output',
52
- 'tmp-export.txt',
53
- ], deps);
54
- strict_1.default.equal(code, 0);
55
- strict_1.default.equal(state.writes.length, 1);
56
- strict_1.default.ok(state.stdout.includes('Exported 2 anchors'));
57
- }
58
- async function testMissingRequiredArgs() {
59
- const { state, deps } = createDeps();
60
- const code = await (0, export_1.runExportCli)(['--format', 'claude-md'], deps);
61
- strict_1.default.equal(code, 1);
62
- strict_1.default.ok(state.stderr.includes('Missing required argument'));
63
- }
64
- async function main() {
65
- await testStdoutMode();
66
- await testOutputMode();
67
- await testMissingRequiredArgs();
68
- process.stdout.write('export-cli.test.ts passed\n');
69
- }
70
- void main();
@@ -1 +0,0 @@
1
- export {};
@@ -1,50 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strict_1 = __importDefault(require("node:assert/strict"));
7
- const shared_1 = require("@spekn/shared");
8
- const args_1 = require("../tui/args");
9
- const policy_1 = require("../tui/capabilities/policy");
10
- function testParseArgs() {
11
- const parsed = (0, args_1.parseTuiArgs)([
12
- '--project-id',
13
- '11111111-1111-4111-8111-111111111111',
14
- '--api-url',
15
- 'http://localhost:9999',
16
- '--view',
17
- 'export',
18
- '--no-color',
19
- ]);
20
- strict_1.default.equal(parsed.projectId, '11111111-1111-4111-8111-111111111111');
21
- strict_1.default.equal(parsed.apiUrl, 'http://localhost:9999');
22
- strict_1.default.equal(parsed.initialView, 'export');
23
- strict_1.default.equal(parsed.noColor, true);
24
- }
25
- function testTierLocks() {
26
- const freePolicy = (0, policy_1.resolveNavPolicy)({
27
- plan: shared_1.OrganizationPlan.FREE,
28
- role: 'member',
29
- workflowPhase: null,
30
- permissions: [],
31
- });
32
- const bridge = freePolicy.find((item) => item.id === 'bridge');
33
- const teamFeature = freePolicy.find((item) => item.id === 'active-runs');
34
- strict_1.default.equal(bridge?.state, 'locked');
35
- strict_1.default.equal(teamFeature?.state, 'locked');
36
- const teamPolicy = (0, policy_1.resolveNavPolicy)({
37
- plan: shared_1.OrganizationPlan.TEAM,
38
- role: 'viewer',
39
- workflowPhase: 'plan',
40
- permissions: [],
41
- });
42
- const gates = teamPolicy.find((item) => item.id === 'phase-gates');
43
- strict_1.default.equal(gates?.state, 'disabled');
44
- }
45
- function main() {
46
- testParseArgs();
47
- testTierLocks();
48
- process.stdout.write('tui-args-policy.test.ts passed\n');
49
- }
50
- main();
@@ -1,23 +0,0 @@
1
- // Bundled TUI entry (ESM) — loaded via dynamic import() from CJS main.js
2
- import {
3
- AGENT_METHODS,
4
- AgentSideConnection,
5
- CLIENT_METHODS,
6
- ClientSideConnection,
7
- PROTOCOL_VERSION,
8
- RequestError,
9
- TerminalHandle,
10
- init_acp,
11
- ndJsonStream
12
- } from "./chunk-M4CS3A25.mjs";
13
- init_acp();
14
- export {
15
- AGENT_METHODS,
16
- AgentSideConnection,
17
- CLIENT_METHODS,
18
- ClientSideConnection,
19
- PROTOCOL_VERSION,
20
- RequestError,
21
- TerminalHandle,
22
- ndJsonStream
23
- };
@@ -1,25 +0,0 @@
1
- // Bundled TUI entry (ESM) — loaded via dynamic import() from CJS main.js
2
- import { createRequire as __createRequire } from 'module';
3
- const require = __createRequire(import.meta.url);
4
- import {
5
- AGENT_METHODS,
6
- AgentSideConnection,
7
- CLIENT_METHODS,
8
- ClientSideConnection,
9
- PROTOCOL_VERSION,
10
- RequestError,
11
- TerminalHandle,
12
- init_acp,
13
- ndJsonStream
14
- } from "./chunk-3PAYRI4G.mjs";
15
- init_acp();
16
- export {
17
- AGENT_METHODS,
18
- AgentSideConnection,
19
- CLIENT_METHODS,
20
- ClientSideConnection,
21
- PROTOCOL_VERSION,
22
- RequestError,
23
- TerminalHandle,
24
- ndJsonStream
25
- };
@@ -1,2 +0,0 @@
1
- export { CredentialsStore } from "@spekn/shared";
2
- export type { CliCredentials } from "@spekn/shared";
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CredentialsStore = void 0;
4
- var shared_1 = require("@spekn/shared");
5
- Object.defineProperty(exports, "CredentialsStore", { enumerable: true, get: function () { return shared_1.CredentialsStore; } });
@@ -1,36 +0,0 @@
1
- /**
2
- * RFC 8628 Device Authorization Grant (Device Flow)
3
- *
4
- * Implements the OAuth 2.0 Device Authorization Grant for CLI authentication
5
- * against a Keycloak realm.
6
- *
7
- * @see https://www.rfc-editor.org/rfc/rfc8628
8
- */
9
- export interface DeviceFlowResult {
10
- accessToken: string;
11
- refreshToken: string;
12
- expiresIn: number;
13
- idToken?: string;
14
- }
15
- export interface DeviceFlowDeps {
16
- stdout: {
17
- write(s: string): void;
18
- };
19
- stderr: {
20
- write(s: string): void;
21
- };
22
- openBrowser: (url: string) => Promise<void>;
23
- }
24
- /** Default browser opener — dynamically imports the `open` package. */
25
- declare const defaultOpenBrowser: (url: string) => Promise<void>;
26
- /**
27
- * Perform the RFC 8628 Device Authorization Grant against a Keycloak realm.
28
- *
29
- * @param keycloakUrl - Base Keycloak URL, e.g. `https://auth.example.com`
30
- * @param realm - Keycloak realm name
31
- * @param clientId - OAuth2 client ID (must be a public client)
32
- * @param deps - Optional dependency overrides for testing / custom I/O
33
- * @returns Resolved tokens once the user authorises the device
34
- */
35
- export declare function performDeviceFlow(keycloakUrl: string, realm: string, clientId: string, deps?: Partial<DeviceFlowDeps>): Promise<DeviceFlowResult>;
36
- export { defaultOpenBrowser };
@@ -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
- }