@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.
- package/README.md +58 -0
- package/dist/main.js +3707 -611
- package/dist/tui/index.mjs +2 -2
- package/package.json +29 -12
- package/dist/__tests__/export-cli.test.d.ts +0 -1
- package/dist/__tests__/export-cli.test.js +0 -70
- package/dist/__tests__/tui-args-policy.test.d.ts +0 -1
- package/dist/__tests__/tui-args-policy.test.js +0 -50
- package/dist/acp-S2MHZOAD.mjs +0 -23
- package/dist/acp-UCCI44JY.mjs +0 -25
- package/dist/auth/credentials-store.d.ts +0 -2
- package/dist/auth/credentials-store.js +0 -5
- package/dist/auth/device-flow.d.ts +0 -36
- package/dist/auth/device-flow.js +0 -189
- package/dist/auth/jwt.d.ts +0 -1
- package/dist/auth/jwt.js +0 -6
- package/dist/auth/session.d.ts +0 -67
- package/dist/auth/session.js +0 -86
- package/dist/auth-login.d.ts +0 -34
- package/dist/auth-login.js +0 -202
- package/dist/auth-logout.d.ts +0 -25
- package/dist/auth-logout.js +0 -115
- package/dist/auth-status.d.ts +0 -24
- package/dist/auth-status.js +0 -109
- package/dist/backlog-generate.d.ts +0 -11
- package/dist/backlog-generate.js +0 -308
- package/dist/backlog-health.d.ts +0 -11
- package/dist/backlog-health.js +0 -287
- package/dist/bridge-login.d.ts +0 -40
- package/dist/bridge-login.js +0 -277
- package/dist/chunk-3PAYRI4G.mjs +0 -2428
- package/dist/chunk-M4CS3A25.mjs +0 -2426
- package/dist/commands/auth/login.d.ts +0 -30
- package/dist/commands/auth/login.js +0 -164
- package/dist/commands/auth/logout.d.ts +0 -25
- package/dist/commands/auth/logout.js +0 -115
- package/dist/commands/auth/status.d.ts +0 -24
- package/dist/commands/auth/status.js +0 -109
- package/dist/commands/backlog/generate.d.ts +0 -11
- package/dist/commands/backlog/generate.js +0 -308
- package/dist/commands/backlog/health.d.ts +0 -11
- package/dist/commands/backlog/health.js +0 -287
- package/dist/commands/bridge/login.d.ts +0 -36
- package/dist/commands/bridge/login.js +0 -258
- package/dist/commands/export.d.ts +0 -35
- package/dist/commands/export.js +0 -485
- package/dist/commands/marketplace-export.d.ts +0 -21
- package/dist/commands/marketplace-export.js +0 -214
- package/dist/commands/project-clean.d.ts +0 -1
- package/dist/commands/project-clean.js +0 -126
- package/dist/commands/repo/common.d.ts +0 -105
- package/dist/commands/repo/common.js +0 -775
- package/dist/commands/repo/detach.d.ts +0 -2
- package/dist/commands/repo/detach.js +0 -120
- package/dist/commands/repo/register.d.ts +0 -21
- package/dist/commands/repo/register.js +0 -175
- package/dist/commands/repo/sync.d.ts +0 -22
- package/dist/commands/repo/sync.js +0 -873
- package/dist/commands/skills-import-local.d.ts +0 -16
- package/dist/commands/skills-import-local.js +0 -352
- package/dist/commands/spec/drift-check.d.ts +0 -3
- package/dist/commands/spec/drift-check.js +0 -186
- package/dist/commands/spec/frontmatter.d.ts +0 -11
- package/dist/commands/spec/frontmatter.js +0 -219
- package/dist/commands/spec/lint.d.ts +0 -11
- package/dist/commands/spec/lint.js +0 -499
- package/dist/commands/spec/parse.d.ts +0 -11
- package/dist/commands/spec/parse.js +0 -162
- package/dist/export.d.ts +0 -35
- package/dist/export.js +0 -485
- package/dist/main.d.ts +0 -1
- package/dist/marketplace-export.d.ts +0 -21
- package/dist/marketplace-export.js +0 -214
- package/dist/project-clean.d.ts +0 -1
- package/dist/project-clean.js +0 -126
- package/dist/project-context.d.ts +0 -99
- package/dist/project-context.js +0 -376
- package/dist/repo-common.d.ts +0 -101
- package/dist/repo-common.js +0 -671
- package/dist/repo-detach.d.ts +0 -2
- package/dist/repo-detach.js +0 -102
- package/dist/repo-ingest.d.ts +0 -29
- package/dist/repo-ingest.js +0 -305
- package/dist/repo-register.d.ts +0 -21
- package/dist/repo-register.js +0 -175
- package/dist/repo-sync.d.ts +0 -16
- package/dist/repo-sync.js +0 -152
- package/dist/resources/prompt-loader.d.ts +0 -1
- package/dist/resources/prompt-loader.js +0 -62
- package/dist/skills-import-local.d.ts +0 -16
- package/dist/skills-import-local.js +0 -352
- package/dist/spec-drift-check.d.ts +0 -3
- package/dist/spec-drift-check.js +0 -186
- package/dist/spec-frontmatter.d.ts +0 -11
- package/dist/spec-frontmatter.js +0 -219
- package/dist/spec-lint.d.ts +0 -11
- package/dist/spec-lint.js +0 -499
- package/dist/spec-parse.d.ts +0 -11
- package/dist/spec-parse.js +0 -162
- package/dist/stubs/dotenv.d.ts +0 -5
- package/dist/stubs/dotenv.js +0 -6
- package/dist/stubs/typeorm.d.ts +0 -22
- package/dist/stubs/typeorm.js +0 -28
- package/dist/tui/app.d.ts +0 -7
- package/dist/tui/app.js +0 -122
- package/dist/tui/args.d.ts +0 -8
- package/dist/tui/args.js +0 -57
- package/dist/tui/capabilities/policy.d.ts +0 -7
- package/dist/tui/capabilities/policy.js +0 -64
- package/dist/tui/components/frame.d.ts +0 -8
- package/dist/tui/components/frame.js +0 -8
- package/dist/tui/components/status-bar.d.ts +0 -8
- package/dist/tui/components/status-bar.js +0 -8
- package/dist/tui/index.d.ts +0 -2
- package/dist/tui/index.js +0 -23
- package/dist/tui/keymap/use-global-keymap.d.ts +0 -19
- package/dist/tui/keymap/use-global-keymap.js +0 -82
- package/dist/tui/navigation/nav-items.d.ts +0 -3
- package/dist/tui/navigation/nav-items.js +0 -18
- package/dist/tui/screens/bridge.d.ts +0 -8
- package/dist/tui/screens/bridge.js +0 -19
- package/dist/tui/screens/decisions.d.ts +0 -5
- package/dist/tui/screens/decisions.js +0 -28
- package/dist/tui/screens/export.d.ts +0 -5
- package/dist/tui/screens/export.js +0 -16
- package/dist/tui/screens/home.d.ts +0 -5
- package/dist/tui/screens/home.js +0 -33
- package/dist/tui/screens/locked.d.ts +0 -5
- package/dist/tui/screens/locked.js +0 -9
- package/dist/tui/screens/specs.d.ts +0 -5
- package/dist/tui/screens/specs.js +0 -31
- package/dist/tui/services/client.d.ts +0 -1
- package/dist/tui/services/client.js +0 -18
- package/dist/tui/services/context-service.d.ts +0 -19
- package/dist/tui/services/context-service.js +0 -246
- package/dist/tui/shared-enums.d.ts +0 -16
- package/dist/tui/shared-enums.js +0 -19
- package/dist/tui/state/use-app-state.d.ts +0 -35
- package/dist/tui/state/use-app-state.js +0 -177
- package/dist/tui/types.d.ts +0 -77
- package/dist/tui/types.js +0 -2
- package/dist/tui-bundle.d.ts +0 -1
- package/dist/tui-bundle.js +0 -5
- package/dist/tui-entry.mjs +0 -1407
- package/dist/utils/cli-runtime.d.ts +0 -5
- package/dist/utils/cli-runtime.js +0 -22
- package/dist/utils/help-error.d.ts +0 -7
- package/dist/utils/help-error.js +0 -14
- package/dist/utils/interaction.d.ts +0 -19
- package/dist/utils/interaction.js +0 -93
- package/dist/utils/structured-log.d.ts +0 -7
- package/dist/utils/structured-log.js +0 -112
- package/dist/utils/trpc-url.d.ts +0 -4
- package/dist/utils/trpc-url.js +0 -15
package/dist/repo-sync.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* repo-sync CLI command
|
|
5
|
-
*
|
|
6
|
-
* Syncs metadata for the current git repository with Spekn.
|
|
7
|
-
* Looks up the repo by its remote URL and updates name and default branch.
|
|
8
|
-
* Optionally runs AI analysis (same as repo register --analyze).
|
|
9
|
-
* Must be run from inside a local git clone.
|
|
10
|
-
*
|
|
11
|
-
* Usage: spekn repo sync --project-id <uuid> [--analyze] [--agent <name>] [--api-url <url>]
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.runRepoSyncCli = runRepoSyncCli;
|
|
15
|
-
exports.main = main;
|
|
16
|
-
exports.parseArgs = parseArgs;
|
|
17
|
-
const repo_common_1 = require("./repo-common");
|
|
18
|
-
const structured_log_1 = require("./utils/structured-log");
|
|
19
|
-
// ── Help & Args ─────────────────────────────────────────────────────
|
|
20
|
-
function printHelp(stderr) {
|
|
21
|
-
stderr(`
|
|
22
|
-
repo sync - Sync git repository metadata with Spekn
|
|
23
|
-
|
|
24
|
-
USAGE:
|
|
25
|
-
spekn repo sync --project-id <uuid> [options]
|
|
26
|
-
|
|
27
|
-
OPTIONS:
|
|
28
|
-
--project-id <uuid> Project ID that owns the repository (optional if .spekn/context is present)
|
|
29
|
-
--analyze Run AI analysis after syncing metadata
|
|
30
|
-
--agent <name> Force a specific agent (claude, codex, opencode, etc.)
|
|
31
|
-
--path <dir> Repository root path (default: current directory)
|
|
32
|
-
--dry-run Discover files only, skip AI analysis
|
|
33
|
-
--mcp-url <url> MCP HTTP server URL (default: MCP_HTTP_URL or https://app.spekn.com/mcp)
|
|
34
|
-
--api-url <url> API base URL (default: SPEKN_API_URL or https://app.spekn.com)
|
|
35
|
-
--debug Show detailed debug output (tokens, HTTP exchanges)
|
|
36
|
-
--help Show this help message
|
|
37
|
-
|
|
38
|
-
ENVIRONMENT:
|
|
39
|
-
SPEKN_API_URL API base URL
|
|
40
|
-
SPEKN_AUTH_TOKEN Bearer token for authentication
|
|
41
|
-
SPEKN_ORGANIZATION_ID Organization ID header
|
|
42
|
-
MCP_HTTP_URL MCP HTTP server URL (default: https://app.spekn.com/mcp)
|
|
43
|
-
AGENT_ARGS Override agent CLI args (skips ACP registry resolution)
|
|
44
|
-
|
|
45
|
-
DESCRIPTION:
|
|
46
|
-
Reads the 'origin' remote URL from the current git repository, looks up the
|
|
47
|
-
matching registered repository in the project, and updates its name and
|
|
48
|
-
default branch to match the local git state.
|
|
49
|
-
|
|
50
|
-
With --analyze, also runs AI-powered analysis to discover specs, assess
|
|
51
|
-
governance, and create improvement specifications (same as repo register).
|
|
52
|
-
|
|
53
|
-
EXAMPLES:
|
|
54
|
-
spekn repo sync --project-id 11111111-1111-4111-8111-111111111111
|
|
55
|
-
spekn repo sync --project-id 11111111-1111-4111-8111-111111111111 --analyze
|
|
56
|
-
spekn repo sync --project-id 11111111-1111-4111-8111-111111111111 --analyze --agent claude
|
|
57
|
-
`);
|
|
58
|
-
}
|
|
59
|
-
function parseArgs(args) {
|
|
60
|
-
const opts = (0, repo_common_1.commonDefaults)(false);
|
|
61
|
-
for (let i = 0; i < args.length;) {
|
|
62
|
-
const consumed = (0, repo_common_1.parseCommonFlag)(args, i, opts);
|
|
63
|
-
if (consumed > 0) {
|
|
64
|
-
i += consumed;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
i++; // skip unknown
|
|
68
|
-
}
|
|
69
|
-
return (0, repo_common_1.finalizeOptions)(opts);
|
|
70
|
-
}
|
|
71
|
-
// ── Main ────────────────────────────────────────────────────────────
|
|
72
|
-
async function runRepoSyncCli(args, deps = repo_common_1.defaultDeps) {
|
|
73
|
-
try {
|
|
74
|
-
const options = parseArgs(args);
|
|
75
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
76
|
-
source: "cli.repo.sync",
|
|
77
|
-
level: "info",
|
|
78
|
-
message: "Starting repo sync",
|
|
79
|
-
details: { repoPath: options.repoPath, analyze: options.analyze, apiUrl: options.apiUrl },
|
|
80
|
-
});
|
|
81
|
-
const { authToken, organizationId, projectId } = await (0, repo_common_1.resolveAuth)(deps, {
|
|
82
|
-
projectId: options.projectId,
|
|
83
|
-
repoPath: options.repoPath,
|
|
84
|
-
});
|
|
85
|
-
// ── Phase 1: Sync repository metadata ──────────────────────────
|
|
86
|
-
const git = (0, repo_common_1.readGitMetadata)(options.repoPath, deps);
|
|
87
|
-
if (!git)
|
|
88
|
-
return 1;
|
|
89
|
-
deps.stdout(`Syncing repository "${git.name}" (${git.remoteUrl})\n`);
|
|
90
|
-
deps.stdout(` Default branch : ${git.defaultBranch}\n`);
|
|
91
|
-
deps.stdout(` Project : ${projectId}\n`);
|
|
92
|
-
const client = (0, repo_common_1.createApiClient)(options.apiUrl, authToken, organizationId);
|
|
93
|
-
// Find matching repository
|
|
94
|
-
const repos =
|
|
95
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
-
await client.gitRepository.list.query({
|
|
97
|
-
projectId,
|
|
98
|
-
limit: 100,
|
|
99
|
-
offset: 0,
|
|
100
|
-
});
|
|
101
|
-
const match = repos.find((r) => r.repositoryUrl === git.remoteUrl);
|
|
102
|
-
if (!match) {
|
|
103
|
-
deps.stderr(`Error: No registered repository found for URL "${git.remoteUrl}" in project ${projectId}.\n` +
|
|
104
|
-
"Use 'spekn repo register' to register this repository first.\n");
|
|
105
|
-
return 1;
|
|
106
|
-
}
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
-
await client.gitRepository.update.mutate({
|
|
109
|
-
projectId,
|
|
110
|
-
id: match.id,
|
|
111
|
-
data: { name: git.name, defaultBranch: git.defaultBranch },
|
|
112
|
-
});
|
|
113
|
-
deps.stdout(`Repository synced successfully. ID: ${match.id}\n`);
|
|
114
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
115
|
-
source: "cli.repo.sync",
|
|
116
|
-
level: "info",
|
|
117
|
-
message: "Repository metadata synced",
|
|
118
|
-
details: { projectId, repositoryId: match.id },
|
|
119
|
-
});
|
|
120
|
-
// ── Phase 2: AI analysis (optional) ─────────────────────────────
|
|
121
|
-
if (!options.analyze)
|
|
122
|
-
return 0;
|
|
123
|
-
const exitCode = await (0, repo_common_1.runAnalysisPhase)({ ...options, projectId }, authToken ?? "", deps, organizationId);
|
|
124
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
125
|
-
source: "cli.repo.sync",
|
|
126
|
-
level: exitCode === 0 ? "info" : "error",
|
|
127
|
-
message: "Repo sync completed",
|
|
128
|
-
details: { exitCode, analyzed: options.analyze, projectId },
|
|
129
|
-
});
|
|
130
|
-
return exitCode;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
if (error instanceof repo_common_1.HelpRequestedError) {
|
|
134
|
-
printHelp(deps.stderr);
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
137
|
-
deps.stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
138
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
139
|
-
source: "cli.repo.sync",
|
|
140
|
-
level: "error",
|
|
141
|
-
message: error instanceof Error ? error.message : String(error),
|
|
142
|
-
});
|
|
143
|
-
return 1;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function main() {
|
|
147
|
-
const exitCode = await runRepoSyncCli(process.argv.slice(2));
|
|
148
|
-
process.exit(exitCode);
|
|
149
|
-
}
|
|
150
|
-
if (require.main === module) {
|
|
151
|
-
void main();
|
|
152
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function loadPromptTemplate(name: string): string;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.loadPromptTemplate = loadPromptTemplate;
|
|
37
|
-
const fs = __importStar(require("node:fs"));
|
|
38
|
-
const path = __importStar(require("node:path"));
|
|
39
|
-
const promptCache = new Map();
|
|
40
|
-
function resolvePromptPath(name) {
|
|
41
|
-
const candidates = [
|
|
42
|
-
path.resolve(__dirname, "resources", "prompts", name),
|
|
43
|
-
path.resolve(__dirname, "prompts", name),
|
|
44
|
-
path.resolve(__dirname, "..", "resources", "prompts", name),
|
|
45
|
-
path.resolve(process.cwd(), "packages", "cli", "src", "resources", "prompts", name),
|
|
46
|
-
];
|
|
47
|
-
for (const candidate of candidates) {
|
|
48
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
49
|
-
return candidate;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`Prompt resource "${name}" not found. Looked in: ${candidates.join(", ")}`);
|
|
53
|
-
}
|
|
54
|
-
function loadPromptTemplate(name) {
|
|
55
|
-
const cached = promptCache.get(name);
|
|
56
|
-
if (cached)
|
|
57
|
-
return cached;
|
|
58
|
-
const promptPath = resolvePromptPath(name);
|
|
59
|
-
const content = fs.readFileSync(promptPath, "utf-8");
|
|
60
|
-
promptCache.set(name, content);
|
|
61
|
-
return content;
|
|
62
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* skills import-local CLI command
|
|
3
|
-
*
|
|
4
|
-
* Imports SKILL.md files from a local directory into a Spekn project.
|
|
5
|
-
*
|
|
6
|
-
* Usage: spekn skills import-local <path> --project-id <uuid> [--namespace <ns>] [--api-url <url>]
|
|
7
|
-
*/
|
|
8
|
-
import { CredentialsStore } from "./auth/credentials-store";
|
|
9
|
-
interface Deps {
|
|
10
|
-
stdout: (content: string) => void;
|
|
11
|
-
stderr: (content: string) => void;
|
|
12
|
-
credentialsStore: CredentialsStore;
|
|
13
|
-
}
|
|
14
|
-
export declare function runSkillsImportLocalCli(args: string[], deps?: Deps): Promise<number>;
|
|
15
|
-
export declare function main(): Promise<void>;
|
|
16
|
-
export {};
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* skills import-local CLI command
|
|
4
|
-
*
|
|
5
|
-
* Imports SKILL.md files from a local directory into a Spekn project.
|
|
6
|
-
*
|
|
7
|
-
* Usage: spekn skills import-local <path> --project-id <uuid> [--namespace <ns>] [--api-url <url>]
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.runSkillsImportLocalCli = runSkillsImportLocalCli;
|
|
11
|
-
exports.main = main;
|
|
12
|
-
const node_fs_1 = require("node:fs");
|
|
13
|
-
const node_path_1 = require("node:path");
|
|
14
|
-
const client_1 = require("@trpc/client");
|
|
15
|
-
const shared_1 = require("@spekn/shared");
|
|
16
|
-
const credentials_store_1 = require("./auth/credentials-store");
|
|
17
|
-
const project_context_1 = require("./project-context");
|
|
18
|
-
const structured_log_1 = require("./utils/structured-log");
|
|
19
|
-
const trpc_url_1 = require("./utils/trpc-url");
|
|
20
|
-
const help_error_1 = require("./utils/help-error");
|
|
21
|
-
const defaultDeps = {
|
|
22
|
-
stdout: (content) => process.stdout.write(content),
|
|
23
|
-
stderr: (content) => process.stderr.write(content),
|
|
24
|
-
credentialsStore: new credentials_store_1.CredentialsStore(),
|
|
25
|
-
};
|
|
26
|
-
function printHelp(stderr) {
|
|
27
|
-
stderr(`
|
|
28
|
-
skills import-local - Import SKILL.md files from a local directory
|
|
29
|
-
|
|
30
|
-
USAGE:
|
|
31
|
-
spekn skills import-local <path> --project-id <uuid> [options]
|
|
32
|
-
|
|
33
|
-
ARGUMENTS:
|
|
34
|
-
<path> Directory containing SKILL.md files (searched recursively)
|
|
35
|
-
|
|
36
|
-
OPTIONS:
|
|
37
|
-
--project-id <uuid> Project ID to import skills into (optional if .spekn/context is present)
|
|
38
|
-
--namespace <ns> Namespace for imported skills (default: directory name)
|
|
39
|
-
--names <a,b,c> Comma-separated skill names to import (default: all)
|
|
40
|
-
--api-url <url> API base URL (default: SPEKN_API_URL or https://app.spekn.com)
|
|
41
|
-
--help Show this help message
|
|
42
|
-
|
|
43
|
-
ENVIRONMENT:
|
|
44
|
-
SPEKN_API_URL API base URL
|
|
45
|
-
SPEKN_AUTH_TOKEN Bearer token for authentication
|
|
46
|
-
SPEKN_ORGANIZATION_ID Organization ID header
|
|
47
|
-
|
|
48
|
-
EXAMPLES:
|
|
49
|
-
spekn skills import-local ./my-skills --project-id 11111111-1111-4111-8111-111111111111
|
|
50
|
-
spekn skills import-local ./plugins/skills --project-id <uuid> --namespace my-org
|
|
51
|
-
`);
|
|
52
|
-
}
|
|
53
|
-
function parseArgs(args) {
|
|
54
|
-
let path = "";
|
|
55
|
-
let projectId = "";
|
|
56
|
-
let namespace = "";
|
|
57
|
-
let names = [];
|
|
58
|
-
let apiUrl = process.env.SPEKN_API_URL ?? "https://app.spekn.com";
|
|
59
|
-
for (let index = 0; index < args.length; index++) {
|
|
60
|
-
const arg = args[index];
|
|
61
|
-
if (arg === "--help" || arg === "-h") {
|
|
62
|
-
throw new help_error_1.HelpRequestedError();
|
|
63
|
-
}
|
|
64
|
-
if (arg === "--project-id" && args[index + 1]) {
|
|
65
|
-
projectId = args[++index];
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (arg?.startsWith("--project-id=")) {
|
|
69
|
-
projectId = arg.slice("--project-id=".length);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (arg === "--namespace" && args[index + 1]) {
|
|
73
|
-
namespace = args[++index];
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (arg?.startsWith("--namespace=")) {
|
|
77
|
-
namespace = arg.slice("--namespace=".length);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (arg === "--names" && args[index + 1]) {
|
|
81
|
-
names = args[++index].split(",").map((n) => n.trim()).filter(Boolean);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (arg?.startsWith("--names=")) {
|
|
85
|
-
names = arg.slice("--names=".length).split(",").map((n) => n.trim()).filter(Boolean);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (arg === "--api-url" && args[index + 1]) {
|
|
89
|
-
apiUrl = args[++index];
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (arg?.startsWith("--api-url=")) {
|
|
93
|
-
apiUrl = arg.slice("--api-url=".length);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
// Positional argument: path
|
|
97
|
-
if (!arg?.startsWith("--") && !path) {
|
|
98
|
-
path = arg;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!path) {
|
|
102
|
-
throw new Error("Missing required argument: <path>");
|
|
103
|
-
}
|
|
104
|
-
const resolvedPath = (0, node_path_1.resolve)(path);
|
|
105
|
-
if (!namespace) {
|
|
106
|
-
namespace = (0, node_path_1.basename)(resolvedPath);
|
|
107
|
-
}
|
|
108
|
-
return { path: resolvedPath, projectId, namespace, names, apiUrl };
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Recursively find all SKILL.md files under a directory.
|
|
112
|
-
*/
|
|
113
|
-
function findSkillFiles(dir) {
|
|
114
|
-
const results = [];
|
|
115
|
-
const entries = (0, node_fs_1.readdirSync)(dir);
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
const fullPath = (0, node_path_1.join)(dir, entry);
|
|
118
|
-
const stat = (0, node_fs_1.statSync)(fullPath);
|
|
119
|
-
if (stat.isDirectory()) {
|
|
120
|
-
results.push(...findSkillFiles(fullPath));
|
|
121
|
-
}
|
|
122
|
-
else if (entry === "SKILL.md") {
|
|
123
|
-
results.push(fullPath);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return results;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Recursively collect all files in a skill directory and return a map
|
|
130
|
-
* of relative paths to file contents.
|
|
131
|
-
*/
|
|
132
|
-
function collectSkillFiles(skillDir) {
|
|
133
|
-
const files = {};
|
|
134
|
-
function walk(dir) {
|
|
135
|
-
const entries = (0, node_fs_1.readdirSync)(dir);
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
const fullPath = (0, node_path_1.join)(dir, entry);
|
|
138
|
-
const stat = (0, node_fs_1.statSync)(fullPath);
|
|
139
|
-
if (stat.isDirectory()) {
|
|
140
|
-
walk(fullPath);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const relPath = (0, node_path_1.relative)(skillDir, fullPath);
|
|
144
|
-
files[relPath] = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
walk(skillDir);
|
|
149
|
-
return files;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Parse a SKILL.md file: extract YAML frontmatter and body content.
|
|
153
|
-
*/
|
|
154
|
-
function parseSkillFile(filePath) {
|
|
155
|
-
const content = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
156
|
-
const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
157
|
-
if (!fmMatch?.[1]) {
|
|
158
|
-
return { error: `${filePath}: no frontmatter found` };
|
|
159
|
-
}
|
|
160
|
-
// Simple YAML parsing without js-yaml dependency.
|
|
161
|
-
// SKILL.md frontmatter is flat key-value, so we parse it manually.
|
|
162
|
-
const yamlText = fmMatch[1];
|
|
163
|
-
const frontmatter = {};
|
|
164
|
-
let currentKey = "";
|
|
165
|
-
let inNestedMap = false;
|
|
166
|
-
const nestedMap = {};
|
|
167
|
-
for (const line of yamlText.split("\n")) {
|
|
168
|
-
// Nested map value (indented)
|
|
169
|
-
if (inNestedMap && /^\s{2,}\S/.test(line)) {
|
|
170
|
-
const kvMatch = /^\s+(\S+):\s*(.*)$/.exec(line);
|
|
171
|
-
if (kvMatch) {
|
|
172
|
-
nestedMap[kvMatch[1]] = kvMatch[2].replace(/^["']|["']$/g, "");
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
// Flush nested map if we were in one
|
|
177
|
-
if (inNestedMap) {
|
|
178
|
-
frontmatter[currentKey] = { ...nestedMap };
|
|
179
|
-
for (const k of Object.keys(nestedMap))
|
|
180
|
-
delete nestedMap[k];
|
|
181
|
-
inNestedMap = false;
|
|
182
|
-
}
|
|
183
|
-
// Top-level key: value
|
|
184
|
-
const topMatch = /^(\S+):\s*(.*)$/.exec(line);
|
|
185
|
-
if (topMatch) {
|
|
186
|
-
const key = topMatch[1];
|
|
187
|
-
const value = topMatch[2].replace(/^["']|["']$/g, "");
|
|
188
|
-
if (value === "" || value === undefined) {
|
|
189
|
-
// Could be start of a nested map
|
|
190
|
-
currentKey = key;
|
|
191
|
-
inNestedMap = true;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
frontmatter[key] = value;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Flush last nested map
|
|
199
|
-
if (inNestedMap) {
|
|
200
|
-
frontmatter[currentKey] = { ...nestedMap };
|
|
201
|
-
}
|
|
202
|
-
// Extract readme (content after frontmatter)
|
|
203
|
-
const readme = content.slice(fmMatch[0].length).trim();
|
|
204
|
-
return { frontmatter, readme };
|
|
205
|
-
}
|
|
206
|
-
async function runSkillsImportLocalCli(args, deps = defaultDeps) {
|
|
207
|
-
try {
|
|
208
|
-
const options = parseArgs(args);
|
|
209
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
210
|
-
source: "cli.skills.import-local",
|
|
211
|
-
level: "info",
|
|
212
|
-
message: "Starting skills import-local",
|
|
213
|
-
details: { path: options.path, namespace: options.namespace, apiUrl: options.apiUrl },
|
|
214
|
-
});
|
|
215
|
-
// Find SKILL.md files
|
|
216
|
-
const skillFiles = findSkillFiles(options.path);
|
|
217
|
-
if (skillFiles.length === 0) {
|
|
218
|
-
deps.stderr(`No SKILL.md files found under ${options.path}\n`);
|
|
219
|
-
return 1;
|
|
220
|
-
}
|
|
221
|
-
deps.stdout(`Found ${skillFiles.length} SKILL.md file(s) in ${options.path}\n`);
|
|
222
|
-
// Parse and validate each file
|
|
223
|
-
const skills = [];
|
|
224
|
-
const parseErrors = [];
|
|
225
|
-
for (const filePath of skillFiles) {
|
|
226
|
-
const result = parseSkillFile(filePath);
|
|
227
|
-
if ("error" in result) {
|
|
228
|
-
parseErrors.push(result.error);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const validation = shared_1.SkillFrontmatterSchema.safeParse(result.frontmatter);
|
|
232
|
-
if (!validation.success) {
|
|
233
|
-
const issues = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
234
|
-
parseErrors.push(`${filePath}: ${issues.join("; ")}`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const fm = validation.data;
|
|
238
|
-
const meta = fm.metadata;
|
|
239
|
-
const skillDir = (0, node_path_1.dirname)(filePath);
|
|
240
|
-
const files = collectSkillFiles(skillDir);
|
|
241
|
-
skills.push({
|
|
242
|
-
name: fm.name,
|
|
243
|
-
description: fm.description,
|
|
244
|
-
version: meta?.version || "0.0.0",
|
|
245
|
-
namespace: options.namespace,
|
|
246
|
-
readme: result.readme || undefined,
|
|
247
|
-
license: fm.license,
|
|
248
|
-
compatibility: fm.compatibility,
|
|
249
|
-
author: meta?.author,
|
|
250
|
-
tags: [],
|
|
251
|
-
metadata: meta ? { ...meta } : undefined,
|
|
252
|
-
files,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
if (parseErrors.length > 0) {
|
|
256
|
-
deps.stderr(`\nParse errors:\n`);
|
|
257
|
-
for (const err of parseErrors) {
|
|
258
|
-
deps.stderr(` - ${err}\n`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Filter by --names if provided
|
|
262
|
-
if (options.names.length > 0) {
|
|
263
|
-
const nameSet = new Set(options.names);
|
|
264
|
-
const before = skills.length;
|
|
265
|
-
const filtered = skills.filter((s) => nameSet.has(s.name));
|
|
266
|
-
skills.length = 0;
|
|
267
|
-
skills.push(...filtered);
|
|
268
|
-
if (skills.length < before) {
|
|
269
|
-
deps.stdout(`Filtered to ${skills.length} of ${before} skill(s) by --names\n`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (skills.length === 0) {
|
|
273
|
-
deps.stderr(`No valid skills to import.\n`);
|
|
274
|
-
return 1;
|
|
275
|
-
}
|
|
276
|
-
deps.stdout(`\nImporting ${skills.length} skill(s)...\n`);
|
|
277
|
-
// Auth
|
|
278
|
-
const storedToken = await deps.credentialsStore.getValidToken();
|
|
279
|
-
const authToken = storedToken ?? process.env.SPEKN_AUTH_TOKEN;
|
|
280
|
-
const storedCreds = deps.credentialsStore.load();
|
|
281
|
-
const context = (0, project_context_1.resolveProjectContext)({
|
|
282
|
-
explicitProjectId: options.projectId,
|
|
283
|
-
repoPath: process.cwd(),
|
|
284
|
-
credentialsOrganizationId: storedCreds?.organizationId,
|
|
285
|
-
envOrganizationId: process.env.SPEKN_ORGANIZATION_ID,
|
|
286
|
-
});
|
|
287
|
-
// tRPC client
|
|
288
|
-
const client = (0, client_1.createTRPCProxyClient)({
|
|
289
|
-
links: [
|
|
290
|
-
(0, client_1.httpBatchLink)({
|
|
291
|
-
url: (0, trpc_url_1.normalizeTrpcUrl)(options.apiUrl),
|
|
292
|
-
headers: {
|
|
293
|
-
"x-organization-id": context.organizationId,
|
|
294
|
-
authorization: authToken ? `Bearer ${authToken}` : "",
|
|
295
|
-
},
|
|
296
|
-
}),
|
|
297
|
-
],
|
|
298
|
-
});
|
|
299
|
-
const result = await client.skills.importLocal.mutate({
|
|
300
|
-
projectId: context.projectId,
|
|
301
|
-
skills,
|
|
302
|
-
});
|
|
303
|
-
// Report
|
|
304
|
-
if (result.imported.length > 0) {
|
|
305
|
-
deps.stdout(`\nImported: ${result.imported.join(", ")}\n`);
|
|
306
|
-
}
|
|
307
|
-
if (result.updated.length > 0) {
|
|
308
|
-
deps.stdout(`Updated: ${result.updated.join(", ")}\n`);
|
|
309
|
-
}
|
|
310
|
-
if (result.errors.length > 0) {
|
|
311
|
-
deps.stderr(`\nServer errors:\n`);
|
|
312
|
-
for (const err of result.errors) {
|
|
313
|
-
deps.stderr(` - ${err}\n`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const total = result.imported.length + result.updated.length;
|
|
317
|
-
deps.stdout(`\nDone. ${total} skill(s) processed.\n`);
|
|
318
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
319
|
-
source: "cli.skills.import-local",
|
|
320
|
-
level: result.errors.length > 0 ? "warn" : "info",
|
|
321
|
-
message: "Skills import-local completed",
|
|
322
|
-
details: {
|
|
323
|
-
totalProcessed: total,
|
|
324
|
-
imported: result.imported.length,
|
|
325
|
-
updated: result.updated.length,
|
|
326
|
-
errors: result.errors.length,
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
return result.errors.length > 0 ? 1 : 0;
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
if (error instanceof help_error_1.HelpRequestedError) {
|
|
333
|
-
printHelp(deps.stderr);
|
|
334
|
-
return 0;
|
|
335
|
-
}
|
|
336
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
-
deps.stderr(`Error: ${message}\n`);
|
|
338
|
-
(0, structured_log_1.appendCliStructuredLog)({
|
|
339
|
-
source: "cli.skills.import-local",
|
|
340
|
-
level: "error",
|
|
341
|
-
message,
|
|
342
|
-
});
|
|
343
|
-
return 1;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
async function main() {
|
|
347
|
-
const exitCode = await runSkillsImportLocalCli(process.argv.slice(2));
|
|
348
|
-
process.exit(exitCode);
|
|
349
|
-
}
|
|
350
|
-
if (require.main === module) {
|
|
351
|
-
void main();
|
|
352
|
-
}
|