@athenode/cli 0.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 +43 -0
- package/dist/apiClient.js +147 -0
- package/dist/commands/auth.js +38 -0
- package/dist/commands/executionResults.js +38 -0
- package/dist/commands/init.js +194 -0
- package/dist/commands/questions.js +50 -0
- package/dist/commands/specs.js +231 -0
- package/dist/config.js +82 -0
- package/dist/env.js +9 -0
- package/dist/env.production.js +9 -0
- package/dist/errors.js +75 -0
- package/dist/fileInstall.js +37 -0
- package/dist/index.js +149 -0
- package/dist/output.js +134 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @athenode/cli
|
|
2
|
+
|
|
3
|
+
The official command-line interface for Athenode — manage your project's specifications,
|
|
4
|
+
track their status, and record implementation results, all without leaving your terminal.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g @athenode/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Getting started
|
|
13
|
+
|
|
14
|
+
Run the interactive setup once in your project:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
athenode init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This walks you through connecting the CLI to your Athenode project and stores a local,
|
|
21
|
+
private config file so you don't have to re-authenticate every time.
|
|
22
|
+
|
|
23
|
+
Already have a project token? Add it directly:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
athenode auth add-token <token>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What you can do
|
|
30
|
+
|
|
31
|
+
- **Browse and search specifications** — list, filter, and full-text search the
|
|
32
|
+
specifications in your project.
|
|
33
|
+
- **Create and edit specifications** — draft new specifications and update their title,
|
|
34
|
+
summary, content, or status right from the command line.
|
|
35
|
+
- **Track implementation plans** — attach or read back the implementation plan for any
|
|
36
|
+
specification.
|
|
37
|
+
- **Record execution results** — log what changed when a specification is implemented.
|
|
38
|
+
- **Manage clarifying questions** — add and answer the questions attached to a
|
|
39
|
+
specification during planning.
|
|
40
|
+
|
|
41
|
+
Run `athenode --help` or `athenode <command> --help` at any time to see everything
|
|
42
|
+
available. Every command also supports a `--json` flag if you'd rather pipe the output
|
|
43
|
+
into another tool.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoActiveAgentSetupError = exports.InvalidTokenError = exports.MissingTokenError = void 0;
|
|
4
|
+
exports.getApiBaseUrl = getApiBaseUrl;
|
|
5
|
+
exports.getStoredToken = getStoredToken;
|
|
6
|
+
exports.getStoredProjectId = getStoredProjectId;
|
|
7
|
+
exports.resolveProjectIdFromToken = resolveProjectIdFromToken;
|
|
8
|
+
exports.listAgentSetups = listAgentSetups;
|
|
9
|
+
exports.getAgentSetupDetail = getAgentSetupDetail;
|
|
10
|
+
exports.getActiveAgentSetupDetail = getActiveAgentSetupDetail;
|
|
11
|
+
exports.apiRequest = apiRequest;
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
const env_1 = require("./env");
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the backend origin from the `ATHENODE_API_URL` env var, falling back to a
|
|
16
|
+
* sensible localhost default. This is a bare origin (no trailing `/api`); callers append
|
|
17
|
+
* their own `/api/...` paths.
|
|
18
|
+
*/
|
|
19
|
+
function getApiBaseUrl() {
|
|
20
|
+
const configured = process.env['ATHENODE_API_URL'];
|
|
21
|
+
const base = configured && configured.trim() ? configured.trim() : env_1.DEFAULT_API_URL;
|
|
22
|
+
return base.endsWith('/') ? base.slice(0, -1) : base;
|
|
23
|
+
}
|
|
24
|
+
/** Reads the stored project token from the project-local config, if any. */
|
|
25
|
+
function getStoredToken() {
|
|
26
|
+
return (0, config_1.readConfig)().token;
|
|
27
|
+
}
|
|
28
|
+
/** Reads the stored project id from the project-local config, if any. */
|
|
29
|
+
function getStoredProjectId() {
|
|
30
|
+
return (0, config_1.readConfig)().projectId;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when a request is made without a stored token. Callers should catch this and
|
|
34
|
+
* instruct the user to run `athenode auth add-token <token>`.
|
|
35
|
+
*/
|
|
36
|
+
class MissingTokenError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('No project token found. Run `athenode auth add-token <token>` first.');
|
|
39
|
+
this.name = 'MissingTokenError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.MissingTokenError = MissingTokenError;
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when a token fails to resolve to a project id (invalid/revoked token, or a
|
|
45
|
+
* non-2xx/malformed response from `GET /api/auth/me`).
|
|
46
|
+
*/
|
|
47
|
+
class InvalidTokenError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'InvalidTokenError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.InvalidTokenError = InvalidTokenError;
|
|
54
|
+
/**
|
|
55
|
+
* Calls `GET /api/auth/me` with `token` directly (rather than via {@link apiRequest}, which
|
|
56
|
+
* reads the token from config — not yet written at the point this is called from `auth
|
|
57
|
+
* add-token`), validating the token and resolving the single project it is scoped to.
|
|
58
|
+
* Throws {@link InvalidTokenError} on any non-2xx response or unexpected body shape.
|
|
59
|
+
*/
|
|
60
|
+
async function resolveProjectIdFromToken(token) {
|
|
61
|
+
const res = await fetch(`${getApiBaseUrl()}/api/auth/me`, {
|
|
62
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
63
|
+
});
|
|
64
|
+
const text = await res.text();
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = text.length ? JSON.parse(text) : undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
parsed = undefined;
|
|
71
|
+
}
|
|
72
|
+
if (res.status < 200 || res.status >= 300) {
|
|
73
|
+
const message = parsed && typeof parsed === 'object' && 'error' in parsed && typeof parsed.error === 'string'
|
|
74
|
+
? parsed.error
|
|
75
|
+
: `request failed with status ${res.status}`;
|
|
76
|
+
throw new InvalidTokenError(message);
|
|
77
|
+
}
|
|
78
|
+
const projectId = parsed && typeof parsed === 'object' ? parsed.projectId : undefined;
|
|
79
|
+
if (typeof projectId !== 'string' || !projectId) {
|
|
80
|
+
throw new InvalidTokenError('unexpected response from the server while resolving the project id');
|
|
81
|
+
}
|
|
82
|
+
return projectId;
|
|
83
|
+
}
|
|
84
|
+
/** Lists the agent setups belonging to `projectId` (no content, just enough to find the active one). */
|
|
85
|
+
async function listAgentSetups(projectId) {
|
|
86
|
+
return apiRequest(`/api/projects/${projectId}/agent-setups`);
|
|
87
|
+
}
|
|
88
|
+
/** Fetches the full detail (agents + commands) of a single agent setup by id. */
|
|
89
|
+
async function getAgentSetupDetail(id) {
|
|
90
|
+
return apiRequest(`/api/agent-setups/${id}`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Thrown when a project has no active agent setup (defensive — every project should have a
|
|
94
|
+
* default setup that is active, but the backend has no dedicated "get my active setup"
|
|
95
|
+
* endpoint, so this composes list + detail and surfaces the absence explicitly).
|
|
96
|
+
*/
|
|
97
|
+
class NoActiveAgentSetupError extends Error {
|
|
98
|
+
constructor(projectId) {
|
|
99
|
+
super(`Project ${projectId} has no active agent setup.`);
|
|
100
|
+
this.name = 'NoActiveAgentSetupError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.NoActiveAgentSetupError = NoActiveAgentSetupError;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the project's active agent setup in full detail, composing the two-hop
|
|
106
|
+
* list-by-project → filter(isActive) → get-detail-by-id calls, since there is no single
|
|
107
|
+
* "get my active setup" endpoint. Throws {@link NoActiveAgentSetupError} if the list request
|
|
108
|
+
* fails or no setup is marked active, and propagates any other non-2xx response as-is via the
|
|
109
|
+
* returned {@link ApiResponse}.
|
|
110
|
+
*/
|
|
111
|
+
async function getActiveAgentSetupDetail(projectId) {
|
|
112
|
+
const listRes = await listAgentSetups(projectId);
|
|
113
|
+
if (listRes.status < 200 || listRes.status >= 300) {
|
|
114
|
+
return { status: listRes.status, body: listRes.body };
|
|
115
|
+
}
|
|
116
|
+
const active = listRes.body.find((setup) => setup.isActive);
|
|
117
|
+
if (!active) {
|
|
118
|
+
throw new NoActiveAgentSetupError(projectId);
|
|
119
|
+
}
|
|
120
|
+
return getAgentSetupDetail(active.id);
|
|
121
|
+
}
|
|
122
|
+
async function apiRequest(path, options = {}) {
|
|
123
|
+
const token = getStoredToken();
|
|
124
|
+
if (!token) {
|
|
125
|
+
throw new MissingTokenError();
|
|
126
|
+
}
|
|
127
|
+
const { method = 'GET', body, headers = {} } = options;
|
|
128
|
+
const url = `${getApiBaseUrl()}${path}`;
|
|
129
|
+
const res = await fetch(url, {
|
|
130
|
+
method,
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${token}`,
|
|
133
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
134
|
+
...headers,
|
|
135
|
+
},
|
|
136
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
137
|
+
});
|
|
138
|
+
const text = await res.text();
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = text.length ? JSON.parse(text) : undefined;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
parsed = text;
|
|
145
|
+
}
|
|
146
|
+
return { status: res.status, body: parsed };
|
|
147
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addTokenCommand = addTokenCommand;
|
|
4
|
+
const apiClient_1 = require("../apiClient");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
/**
|
|
7
|
+
* Implements `athenode auth add-token <token>`: validates an already-minted project
|
|
8
|
+
* token against the backend (`GET /api/auth/me`), which also resolves the single project
|
|
9
|
+
* id it is scoped to, then stores both into the project-local config file. Lazily
|
|
10
|
+
* initializes the config (and `.gitignore` entry) if `init` has not been run yet, so this
|
|
11
|
+
* command works standalone.
|
|
12
|
+
*/
|
|
13
|
+
async function addTokenCommand(token) {
|
|
14
|
+
const trimmed = token.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
console.error('Error: token must not be empty.');
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let projectId;
|
|
21
|
+
try {
|
|
22
|
+
projectId = await (0, apiClient_1.resolveProjectIdFromToken)(trimmed);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const message = err instanceof apiClient_1.InvalidTokenError ? err.message : err instanceof Error ? err.message : String(err);
|
|
26
|
+
console.error(`Error: invalid token (${message})`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const wasInitialized = (0, config_1.configExists)();
|
|
31
|
+
(0, config_1.updateConfig)({ token: trimmed, projectId });
|
|
32
|
+
(0, config_1.ensureGitignoreEntry)();
|
|
33
|
+
if (!wasInitialized) {
|
|
34
|
+
console.log('Initialized .athenode/config.json');
|
|
35
|
+
}
|
|
36
|
+
console.log('Token stored in .athenode/config.json');
|
|
37
|
+
console.log(`Project id: ${projectId} (resolved from token)`);
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executionResultsListCommand = executionResultsListCommand;
|
|
4
|
+
exports.executionResultsCreateCommand = executionResultsCreateCommand;
|
|
5
|
+
const apiClient_1 = require("../apiClient");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const output_1 = require("../output");
|
|
8
|
+
async function executionResultsListCommand(specificationId, options) {
|
|
9
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
10
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${specificationId}/execution-results`);
|
|
11
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (options.json) {
|
|
15
|
+
(0, output_1.printJson)(res.body);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log((0, output_1.formatExecutionResultList)(res.body));
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async function executionResultsCreateCommand(specificationId, options) {
|
|
23
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
24
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${specificationId}/execution-results`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
body: { filePath: options.filePath, summary: options.summary },
|
|
27
|
+
});
|
|
28
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (options.json) {
|
|
32
|
+
(0, output_1.printJson)(res.body);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log((0, output_1.formatExecutionResult)(res.body));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initCommand = initCommand;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const apiClient_1 = require("../apiClient");
|
|
7
|
+
const config_1 = require("../config");
|
|
8
|
+
const errors_1 = require("../errors");
|
|
9
|
+
const fileInstall_1 = require("../fileInstall");
|
|
10
|
+
let clackPromise;
|
|
11
|
+
function clack() {
|
|
12
|
+
if (!clackPromise) {
|
|
13
|
+
clackPromise = import('@clack/prompts');
|
|
14
|
+
}
|
|
15
|
+
return clackPromise;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Implements `athenode init`: an interactive flow that (1) imports/validates a project
|
|
19
|
+
* token if one isn't already stored, (2) fetches the project's active agent setup, (3) lets
|
|
20
|
+
* the user pick which agents to install, and (4) writes the resulting command/agent
|
|
21
|
+
* files into `.claude/commands/` and `.claude/agents/`, prompting on conflicts with files
|
|
22
|
+
* that already exist on disk.
|
|
23
|
+
*
|
|
24
|
+
* Safe/idempotent to re-run: an already-stored token is reused rather than re-prompted for,
|
|
25
|
+
* and files whose on-disk content already matches are left untouched without prompting.
|
|
26
|
+
*/
|
|
27
|
+
async function initCommand() {
|
|
28
|
+
const p = await clack();
|
|
29
|
+
const projectId = await ensureProjectToken(p);
|
|
30
|
+
if (!projectId) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
(0, config_1.ensureGitignoreEntry)();
|
|
34
|
+
let setup;
|
|
35
|
+
try {
|
|
36
|
+
const res = await (0, apiClient_1.getActiveAgentSetupDetail)(projectId);
|
|
37
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setup = res.body;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof apiClient_1.NoActiveAgentSetupError) {
|
|
44
|
+
console.error(`Error: ${err.message}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
let selectedAgents = setup.agents;
|
|
51
|
+
if (setup.agents.length > 0) {
|
|
52
|
+
const agentNames = await p.multiselect({
|
|
53
|
+
message: `Select agents to install from "${setup.name}"`,
|
|
54
|
+
options: setup.agents.map((a) => ({ value: a.name, label: a.name })),
|
|
55
|
+
initialValues: setup.agents.map((a) => a.name),
|
|
56
|
+
required: false,
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(agentNames)) {
|
|
59
|
+
p.cancel('Aborted: no selection made.');
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const selectedNames = new Set(agentNames);
|
|
64
|
+
selectedAgents = setup.agents.filter((a) => selectedNames.has(a.name));
|
|
65
|
+
}
|
|
66
|
+
const plan = [
|
|
67
|
+
...(0, fileInstall_1.planInstall)(setup.commands, '.claude/commands'),
|
|
68
|
+
...(0, fileInstall_1.planInstall)(selectedAgents, '.claude/agents'),
|
|
69
|
+
];
|
|
70
|
+
let written = 0;
|
|
71
|
+
let skipped = 0;
|
|
72
|
+
let unchanged = 0;
|
|
73
|
+
for (const entry of plan) {
|
|
74
|
+
if (!entry.conflict) {
|
|
75
|
+
if (!entry.exists) {
|
|
76
|
+
(0, fileInstall_1.writeInstallEntry)(entry, 'overwrite');
|
|
77
|
+
written += 1;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
unchanged += 1;
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const resolution = await resolveConflict(p, entry);
|
|
85
|
+
if (resolution === 'overwrite') {
|
|
86
|
+
(0, fileInstall_1.writeInstallEntry)(entry, 'overwrite');
|
|
87
|
+
written += 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
skipped += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(`Installed ${written} file(s), skipped ${skipped} conflicting file(s), ${unchanged} already up to date.`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Ensures a project token is stored, prompting for one interactively if not, and returns the
|
|
98
|
+
* resolved project id. Returns `undefined` (with `process.exitCode` set) on failure.
|
|
99
|
+
*/
|
|
100
|
+
async function ensureProjectToken(p) {
|
|
101
|
+
const existingToken = (0, apiClient_1.getStoredToken)();
|
|
102
|
+
const existingProjectId = (0, apiClient_1.getStoredProjectId)();
|
|
103
|
+
if (existingToken && existingProjectId) {
|
|
104
|
+
console.log('Using stored project token (.athenode/config.json).');
|
|
105
|
+
return existingProjectId;
|
|
106
|
+
}
|
|
107
|
+
const wasInitialized = (0, config_1.configExists)();
|
|
108
|
+
const token = await p.password({
|
|
109
|
+
message: 'Enter your athenode project token',
|
|
110
|
+
});
|
|
111
|
+
if (p.isCancel(token)) {
|
|
112
|
+
p.cancel('Aborted.');
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const trimmed = token.trim();
|
|
117
|
+
if (!trimmed) {
|
|
118
|
+
console.error('Error: token must not be empty.');
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
let projectId;
|
|
123
|
+
try {
|
|
124
|
+
projectId = await (0, apiClient_1.resolveProjectIdFromToken)(trimmed);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const message = err instanceof apiClient_1.InvalidTokenError ? err.message : err instanceof Error ? err.message : String(err);
|
|
128
|
+
console.error(`Error: invalid token (${message})`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
(0, config_1.updateConfig)({ token: trimmed, projectId });
|
|
133
|
+
if (!wasInitialized) {
|
|
134
|
+
console.log('Initialized .athenode/config.json');
|
|
135
|
+
}
|
|
136
|
+
console.log(`Token stored. Project id: ${projectId} (resolved from token)`);
|
|
137
|
+
return projectId;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Renders a minimal line-by-line diff between the on-disk file and the incoming content.
|
|
141
|
+
* Deliberately simple (no LCS/word-level alignment, no external diff dependency): lines that
|
|
142
|
+
* differ position-by-position are shown as removed/added pairs, and a trailing length
|
|
143
|
+
* mismatch is reported as pure additions or removals.
|
|
144
|
+
*/
|
|
145
|
+
function renderDiff(existingContent, incomingContent) {
|
|
146
|
+
const existingLines = existingContent.split('\n');
|
|
147
|
+
const incomingLines = incomingContent.split('\n');
|
|
148
|
+
const maxLines = Math.max(existingLines.length, incomingLines.length);
|
|
149
|
+
const out = [];
|
|
150
|
+
for (let i = 0; i < maxLines; i += 1) {
|
|
151
|
+
const existingLine = existingLines[i];
|
|
152
|
+
const incomingLine = incomingLines[i];
|
|
153
|
+
if (existingLine === incomingLine) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (existingLine !== undefined) {
|
|
157
|
+
out.push(`- ${existingLine}`);
|
|
158
|
+
}
|
|
159
|
+
if (incomingLine !== undefined) {
|
|
160
|
+
out.push(`+ ${incomingLine}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out.length > 0 ? out.join('\n') : '(no line-level differences found)';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Prompts the user how to resolve a single conflicting install entry: skip, overwrite, or show
|
|
167
|
+
* a diff first (which re-prompts the same question afterward, since "show diff" isn't itself
|
|
168
|
+
* a resolution).
|
|
169
|
+
*/
|
|
170
|
+
async function resolveConflict(p, entry) {
|
|
171
|
+
for (;;) {
|
|
172
|
+
const resolution = await p.select({
|
|
173
|
+
message: `${entry.targetPath} already exists with different content. What would you like to do?`,
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'overwrite', label: 'Overwrite' },
|
|
176
|
+
{ value: 'skip', label: 'Skip' },
|
|
177
|
+
{ value: 'diff', label: 'Show diff' },
|
|
178
|
+
],
|
|
179
|
+
initialValue: 'skip',
|
|
180
|
+
});
|
|
181
|
+
if (p.isCancel(resolution)) {
|
|
182
|
+
p.cancel('Aborted.');
|
|
183
|
+
return 'skip';
|
|
184
|
+
}
|
|
185
|
+
if (resolution === 'diff') {
|
|
186
|
+
const existingContent = fs.readFileSync(entry.targetPath, 'utf8');
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(renderDiff(existingContent, entry.item.content));
|
|
189
|
+
console.log('');
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
return resolution;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.questionsListCommand = questionsListCommand;
|
|
4
|
+
exports.questionsAddCommand = questionsAddCommand;
|
|
5
|
+
exports.questionsAnswerCommand = questionsAnswerCommand;
|
|
6
|
+
const apiClient_1 = require("../apiClient");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const output_1 = require("../output");
|
|
9
|
+
async function questionsListCommand(specificationId, options) {
|
|
10
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
11
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${specificationId}/questions`);
|
|
12
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (options.json) {
|
|
16
|
+
(0, output_1.printJson)(res.body);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log((0, output_1.formatQuestionList)(res.body));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function questionsAddCommand(specificationId, options) {
|
|
24
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
25
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${specificationId}/questions`, { method: 'POST', body: { question: options.question } });
|
|
26
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (options.json) {
|
|
30
|
+
(0, output_1.printJson)(res.body);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log((0, output_1.formatQuestion)(res.body));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function questionsAnswerCommand(specificationId, questionId, options) {
|
|
38
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
39
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${specificationId}/questions/${questionId}`, { method: 'PATCH', body: { answer: options.answer } });
|
|
40
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (options.json) {
|
|
44
|
+
(0, output_1.printJson)(res.body);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log((0, output_1.formatQuestion)(res.body));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.specsListCommand = specsListCommand;
|
|
4
|
+
exports.specsSearchCommand = specsSearchCommand;
|
|
5
|
+
exports.specsGetCommand = specsGetCommand;
|
|
6
|
+
exports.specsCreateCommand = specsCreateCommand;
|
|
7
|
+
exports.specsUpdateCommand = specsUpdateCommand;
|
|
8
|
+
exports.specsSetStatusCommand = specsSetStatusCommand;
|
|
9
|
+
exports.specsSetPlanCommand = specsSetPlanCommand;
|
|
10
|
+
exports.specsGetPlanCommand = specsGetPlanCommand;
|
|
11
|
+
const apiClient_1 = require("../apiClient");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
const output_1 = require("../output");
|
|
14
|
+
/** Resolves the project id to use for `list`/`search`: the `--project` flag, or the value stored by `auth add-token <token>`. */
|
|
15
|
+
function resolveProjectId(projectOption) {
|
|
16
|
+
return projectOption ?? (0, apiClient_1.getStoredProjectId)();
|
|
17
|
+
}
|
|
18
|
+
async function specsListCommand(options) {
|
|
19
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
20
|
+
const projectId = resolveProjectId(options.project);
|
|
21
|
+
if (!projectId) {
|
|
22
|
+
console.error('Error: no project id available. Pass --project <projectId> or run `athenode auth add-token <token>`.');
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (options.topLevel && options.parent !== undefined) {
|
|
27
|
+
console.error('Error: --parent and --top-level are mutually exclusive.');
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const query = new URLSearchParams();
|
|
32
|
+
if (options.status !== undefined) {
|
|
33
|
+
query.set('status', options.status);
|
|
34
|
+
}
|
|
35
|
+
if (options.parent !== undefined) {
|
|
36
|
+
query.set('parentId', options.parent);
|
|
37
|
+
}
|
|
38
|
+
if (options.topLevel) {
|
|
39
|
+
query.set('topLevel', 'true');
|
|
40
|
+
}
|
|
41
|
+
const queryString = query.toString();
|
|
42
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/projects/${projectId}/specifications${queryString ? `?${queryString}` : ''}`);
|
|
43
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (options.json) {
|
|
47
|
+
(0, output_1.printJson)(res.body);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log((0, output_1.formatSpecificationList)(res.body));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function specsSearchCommand(query, options) {
|
|
55
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
56
|
+
const projectId = resolveProjectId(options.project);
|
|
57
|
+
if (!projectId) {
|
|
58
|
+
console.error('Error: no project id available. Pass --project <projectId> or run `athenode auth add-token <token>`.');
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!query || !query.trim()) {
|
|
63
|
+
console.error('Error: search query must not be empty.');
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/projects/${projectId}/specifications/search?q=${encodeURIComponent(query)}`);
|
|
68
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (options.json) {
|
|
72
|
+
(0, output_1.printJson)(res.body);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log((0, output_1.formatSpecificationSearchResults)(res.body));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function specsGetCommand(id, options) {
|
|
80
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
81
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${id}`);
|
|
82
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (options.json) {
|
|
86
|
+
(0, output_1.printJson)(res.body);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log((0, output_1.formatSpecification)(res.body));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function specsCreateCommand(options) {
|
|
94
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
95
|
+
const projectId = options.project ?? (0, apiClient_1.getStoredProjectId)();
|
|
96
|
+
if (!projectId) {
|
|
97
|
+
console.error('Error: no project id available. Pass --project <projectId> or run `athenode auth add-token <token>`.');
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const body = {
|
|
102
|
+
projectId,
|
|
103
|
+
title: options.title,
|
|
104
|
+
};
|
|
105
|
+
if (options.parent !== undefined) {
|
|
106
|
+
body['parentId'] = options.parent;
|
|
107
|
+
}
|
|
108
|
+
if (options.summary !== undefined) {
|
|
109
|
+
body['summary'] = options.summary;
|
|
110
|
+
}
|
|
111
|
+
if (options.content !== undefined) {
|
|
112
|
+
body['content'] = options.content;
|
|
113
|
+
}
|
|
114
|
+
if (options.status !== undefined) {
|
|
115
|
+
body['status'] = options.status;
|
|
116
|
+
}
|
|
117
|
+
const res = await (0, apiClient_1.apiRequest)('/api/specifications', { method: 'POST', body });
|
|
118
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (options.json) {
|
|
122
|
+
(0, output_1.printJson)(res.body);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log((0, output_1.formatSpecification)(res.body));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Builds a partial-update body containing only fields the user actually supplied, matching the
|
|
131
|
+
* backend's presence-tracked `SpecificationUpdateRequest` semantics (a field's key is omitted
|
|
132
|
+
* entirely unless the user explicitly set or cleared it).
|
|
133
|
+
*/
|
|
134
|
+
function buildUpdateBody(options) {
|
|
135
|
+
const body = {};
|
|
136
|
+
if (options.clearParent) {
|
|
137
|
+
body['parentId'] = null;
|
|
138
|
+
}
|
|
139
|
+
else if (options.parent !== undefined) {
|
|
140
|
+
body['parentId'] = options.parent;
|
|
141
|
+
}
|
|
142
|
+
if (options.title !== undefined) {
|
|
143
|
+
body['title'] = options.title;
|
|
144
|
+
}
|
|
145
|
+
if (options.clearSummary) {
|
|
146
|
+
body['summary'] = null;
|
|
147
|
+
}
|
|
148
|
+
else if (options.summary !== undefined) {
|
|
149
|
+
body['summary'] = options.summary;
|
|
150
|
+
}
|
|
151
|
+
if (options.clearContent) {
|
|
152
|
+
body['content'] = null;
|
|
153
|
+
}
|
|
154
|
+
else if (options.content !== undefined) {
|
|
155
|
+
body['content'] = options.content;
|
|
156
|
+
}
|
|
157
|
+
return body;
|
|
158
|
+
}
|
|
159
|
+
async function specsUpdateCommand(id, options) {
|
|
160
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
161
|
+
const body = buildUpdateBody(options);
|
|
162
|
+
if (Object.keys(body).length === 0) {
|
|
163
|
+
console.error('Error: no fields to update were supplied.');
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${id}`, { method: 'PATCH', body });
|
|
168
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (options.json) {
|
|
172
|
+
(0, output_1.printJson)(res.body);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log((0, output_1.formatSpecification)(res.body));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function specsSetStatusCommand(id, status, options) {
|
|
180
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
181
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${id}`, {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
body: { status },
|
|
184
|
+
});
|
|
185
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (options.json) {
|
|
189
|
+
(0, output_1.printJson)(res.body);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log((0, output_1.formatSpecification)(res.body));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function specsSetPlanCommand(id, options) {
|
|
197
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
198
|
+
if (!options.clearPlan && options.plan === undefined) {
|
|
199
|
+
console.error('Error: pass --plan <text> or --clear-plan.');
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const body = {
|
|
204
|
+
implementationPlan: options.clearPlan ? null : options.plan,
|
|
205
|
+
};
|
|
206
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${id}`, { method: 'PATCH', body });
|
|
207
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (options.json) {
|
|
211
|
+
(0, output_1.printJson)(res.body);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log((0, output_1.formatSpecification)(res.body));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function specsGetPlanCommand(id, options) {
|
|
219
|
+
await (0, errors_1.withErrorHandling)(async () => {
|
|
220
|
+
const res = await (0, apiClient_1.apiRequest)(`/api/specifications/${id}`);
|
|
221
|
+
if ((0, errors_1.handleApiError)(res)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (options.json) {
|
|
225
|
+
(0, output_1.printJson)({ implementationPlan: res.body.implementationPlan ?? null });
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log(res.body.implementationPlan ?? '(no implementation plan set)');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConfigDir = getConfigDir;
|
|
4
|
+
exports.getConfigPath = getConfigPath;
|
|
5
|
+
exports.configExists = configExists;
|
|
6
|
+
exports.readConfig = readConfig;
|
|
7
|
+
exports.writeConfig = writeConfig;
|
|
8
|
+
exports.updateConfig = updateConfig;
|
|
9
|
+
exports.ensureGitignoreEntry = ensureGitignoreEntry;
|
|
10
|
+
const tslib_1 = require("tslib");
|
|
11
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
12
|
+
const path = tslib_1.__importStar(require("path"));
|
|
13
|
+
const CONFIG_DIR_NAME = '.athenode';
|
|
14
|
+
const CONFIG_FILE_NAME = 'config.json';
|
|
15
|
+
const RESTRICTIVE_MODE = 0o600;
|
|
16
|
+
/** Absolute path to the `.athenode` directory for the current working directory. */
|
|
17
|
+
function getConfigDir(cwd = process.cwd()) {
|
|
18
|
+
return path.join(cwd, CONFIG_DIR_NAME);
|
|
19
|
+
}
|
|
20
|
+
/** Absolute path to the `.athenode/config.json` file for the current working directory. */
|
|
21
|
+
function getConfigPath(cwd = process.cwd()) {
|
|
22
|
+
return path.join(getConfigDir(cwd), CONFIG_FILE_NAME);
|
|
23
|
+
}
|
|
24
|
+
/** Whether a config file already exists in the current project. */
|
|
25
|
+
function configExists(cwd = process.cwd()) {
|
|
26
|
+
return fs.existsSync(getConfigPath(cwd));
|
|
27
|
+
}
|
|
28
|
+
/** Reads and parses the config file, returning an empty config if it does not exist. */
|
|
29
|
+
function readConfig(cwd = process.cwd()) {
|
|
30
|
+
const configPath = getConfigPath(cwd);
|
|
31
|
+
if (!fs.existsSync(configPath)) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
35
|
+
if (!raw.trim()) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Writes the config file, creating the `.athenode` directory if needed, and enforcing
|
|
42
|
+
* restrictive (0600) permissions regardless of whether the file previously existed.
|
|
43
|
+
*/
|
|
44
|
+
function writeConfig(config, cwd = process.cwd()) {
|
|
45
|
+
const configDir = getConfigDir(cwd);
|
|
46
|
+
const configPath = getConfigPath(cwd);
|
|
47
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: RESTRICTIVE_MODE });
|
|
49
|
+
// writeFileSync's `mode` option only applies at file-creation time; if the file already
|
|
50
|
+
// existed, its permissions may have been looser, so re-apply them explicitly here.
|
|
51
|
+
fs.chmodSync(configPath, RESTRICTIVE_MODE);
|
|
52
|
+
}
|
|
53
|
+
/** Merges `partial` into the existing config (if any) and writes the result back. */
|
|
54
|
+
function updateConfig(partial, cwd = process.cwd()) {
|
|
55
|
+
const merged = { ...readConfig(cwd), ...partial };
|
|
56
|
+
writeConfig(merged, cwd);
|
|
57
|
+
return merged;
|
|
58
|
+
}
|
|
59
|
+
const GITIGNORE_ENTRY = `${CONFIG_DIR_NAME}/`;
|
|
60
|
+
/**
|
|
61
|
+
* Ensures the project's `.gitignore` (at `cwd`) contains an entry for the `.athenode/`
|
|
62
|
+
* directory. Idempotent: does nothing if an entry is already present. Creates the file if
|
|
63
|
+
* it doesn't exist.
|
|
64
|
+
*/
|
|
65
|
+
function ensureGitignoreEntry(cwd = process.cwd()) {
|
|
66
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
67
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
68
|
+
fs.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const contents = fs.readFileSync(gitignorePath, 'utf8');
|
|
72
|
+
const alreadyIgnored = contents
|
|
73
|
+
.split('\n')
|
|
74
|
+
.map((line) => line.trim())
|
|
75
|
+
.some((line) => line === GITIGNORE_ENTRY || line === CONFIG_DIR_NAME);
|
|
76
|
+
if (alreadyIgnored) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const needsLeadingNewline = contents.length > 0 && !contents.endsWith('\n');
|
|
80
|
+
const suffix = `${needsLeadingNewline ? '\n' : ''}${GITIGNORE_ENTRY}\n`;
|
|
81
|
+
fs.appendFileSync(gitignorePath, suffix);
|
|
82
|
+
}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_API_URL = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Production API origin. Swapped in for `env.ts` by the CI publish job (see
|
|
6
|
+
* .gitlab-ci.yml) before building, so published npm releases point here by default.
|
|
7
|
+
* `ATHENODE_API_URL` always overrides this at runtime.
|
|
8
|
+
*/
|
|
9
|
+
exports.DEFAULT_API_URL = 'https://api.athenode.com';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_API_URL = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Production API origin. Swapped in for `env.ts` by the CI publish job (see
|
|
6
|
+
* .gitlab-ci.yml) before building, so published npm releases point here by default.
|
|
7
|
+
* `ATHENODE_API_URL` always overrides this at runtime.
|
|
8
|
+
*/
|
|
9
|
+
exports.DEFAULT_API_URL = 'https://api.athenode.com';
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleApiError = handleApiError;
|
|
4
|
+
exports.withErrorHandling = withErrorHandling;
|
|
5
|
+
const apiClient_1 = require("./apiClient");
|
|
6
|
+
/**
|
|
7
|
+
* Extracts a human-readable message from a REST error body. The backend's error responses
|
|
8
|
+
* are shaped like `{ "message": "..." }` (or similar); fall back to a generic description if
|
|
9
|
+
* the body doesn't match that shape.
|
|
10
|
+
*/
|
|
11
|
+
function extractMessage(body) {
|
|
12
|
+
if (body && typeof body === 'object' && 'message' in body) {
|
|
13
|
+
const message = body.message;
|
|
14
|
+
if (typeof message === 'string' && message.trim()) {
|
|
15
|
+
return message;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (typeof body === 'string' && body.trim()) {
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Maps a REST response status code to a human-readable error prefix, per the CLI's
|
|
25
|
+
* error-handling convention:
|
|
26
|
+
* 400 -> validation error, 401/403 -> auth error, 404 -> not found, 5xx -> server error.
|
|
27
|
+
*/
|
|
28
|
+
function statusLabel(status) {
|
|
29
|
+
if (status === 400) {
|
|
30
|
+
return 'Validation error';
|
|
31
|
+
}
|
|
32
|
+
if (status === 401 || status === 403) {
|
|
33
|
+
return 'Authentication error';
|
|
34
|
+
}
|
|
35
|
+
if (status === 404) {
|
|
36
|
+
return 'Not found';
|
|
37
|
+
}
|
|
38
|
+
if (status >= 500) {
|
|
39
|
+
return 'Server error';
|
|
40
|
+
}
|
|
41
|
+
return 'Request failed';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks whether `response` represents a REST failure (non-2xx status). If so, prints a
|
|
45
|
+
* consistent error message to stderr, sets `process.exitCode = 1`, and returns `true` so the
|
|
46
|
+
* caller can stop further processing. Returns `false` for 2xx responses.
|
|
47
|
+
*/
|
|
48
|
+
function handleApiError(response) {
|
|
49
|
+
if (response.status >= 200 && response.status < 300) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const message = extractMessage(response.body);
|
|
53
|
+
const label = statusLabel(response.status);
|
|
54
|
+
console.error(`Error: ${label} (${response.status})${message ? `: ${message}` : ''}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Runs `fn`, translating a thrown {@link MissingTokenError} into a consistent CLI error message
|
|
60
|
+
* (rather than an uncaught stack trace) and setting `process.exitCode = 1`.
|
|
61
|
+
*/
|
|
62
|
+
async function withErrorHandling(fn) {
|
|
63
|
+
try {
|
|
64
|
+
await fn();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof apiClient_1.MissingTokenError) {
|
|
68
|
+
console.error(`Error: ${err.message}`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planInstall = planInstall;
|
|
4
|
+
exports.writeInstallEntry = writeInstallEntry;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
8
|
+
/**
|
|
9
|
+
* Plans the on-disk writes for `items` into `targetDir` (relative to `cwd`). Read-only: does
|
|
10
|
+
* not touch the filesystem beyond checking for existing files.
|
|
11
|
+
*/
|
|
12
|
+
function planInstall(items, targetDir, cwd = process.cwd()) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const targetPath = path.join(cwd, targetDir, `${item.name}.md`);
|
|
16
|
+
const exists = fs.existsSync(targetPath);
|
|
17
|
+
let conflict = false;
|
|
18
|
+
if (exists) {
|
|
19
|
+
const existingContent = fs.readFileSync(targetPath, 'utf8');
|
|
20
|
+
conflict = existingContent !== item.content;
|
|
21
|
+
}
|
|
22
|
+
entries.push({ item, targetPath, exists, conflict });
|
|
23
|
+
}
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Writes (or skips) a single planned install entry. `overwrite` creates the target directory
|
|
28
|
+
* as needed and writes `entry.item.content` verbatim; `skip` is a no-op left to the caller
|
|
29
|
+
* to log.
|
|
30
|
+
*/
|
|
31
|
+
function writeInstallEntry(entry, resolution) {
|
|
32
|
+
if (resolution === 'skip') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
fs.mkdirSync(path.dirname(entry.targetPath), { recursive: true });
|
|
36
|
+
fs.writeFileSync(entry.targetPath, entry.item.content);
|
|
37
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const init_1 = require("./commands/init");
|
|
6
|
+
const auth_1 = require("./commands/auth");
|
|
7
|
+
const specs_1 = require("./commands/specs");
|
|
8
|
+
const executionResults_1 = require("./commands/executionResults");
|
|
9
|
+
const questions_1 = require("./commands/questions");
|
|
10
|
+
const program = new commander_1.Command();
|
|
11
|
+
program.name('athenode').description('CLI for athenode: project scaffold, config, and tokens.');
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Interactively set up athenode in this project: import/validate a project token, ' +
|
|
15
|
+
'fetch the active agent setup, pick which sub-agents to install, and write the ' +
|
|
16
|
+
'resulting command/sub-agent files locally (with conflict prompts).')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
await (0, init_1.initCommand)();
|
|
19
|
+
});
|
|
20
|
+
const auth = program.command('auth').description('Manage authentication for the athenode CLI.');
|
|
21
|
+
auth
|
|
22
|
+
.command('add-token <token>')
|
|
23
|
+
.description('Validate an already-minted project token and store it (and its resolved project id) in the project-local config.')
|
|
24
|
+
.action(async (token) => {
|
|
25
|
+
await (0, auth_1.addTokenCommand)(token);
|
|
26
|
+
});
|
|
27
|
+
const specs = program.command('specs').description('Manage specifications via the athenode REST API.');
|
|
28
|
+
specs
|
|
29
|
+
.command('list')
|
|
30
|
+
.description('List all specifications in a project.')
|
|
31
|
+
.option('--project <projectId>', 'Project id (defaults to the one stored via `auth add-token`).')
|
|
32
|
+
.option('--status <status>', 'Filter by status (draft | approved | processing | done).')
|
|
33
|
+
.option('--parent <parentId>', 'Filter by parent specification id.')
|
|
34
|
+
.option('--top-level', 'List only top-level specifications (those with no parent). Mutually exclusive with --parent.')
|
|
35
|
+
.option('--json', 'Print raw JSON instead of a human-readable table.')
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
await (0, specs_1.specsListCommand)(options);
|
|
38
|
+
});
|
|
39
|
+
specs
|
|
40
|
+
.command('search <query>')
|
|
41
|
+
.description('Full-text search specifications in a project.')
|
|
42
|
+
.option('--project <projectId>', 'Project id (defaults to the one stored via `auth add-token`).')
|
|
43
|
+
.option('--json', 'Print raw JSON instead of a human-readable table.')
|
|
44
|
+
.action(async (query, options) => {
|
|
45
|
+
await (0, specs_1.specsSearchCommand)(query, options);
|
|
46
|
+
});
|
|
47
|
+
specs
|
|
48
|
+
.command('get <id>')
|
|
49
|
+
.description('Get a single specification by id.')
|
|
50
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
51
|
+
.action(async (id, options) => {
|
|
52
|
+
await (0, specs_1.specsGetCommand)(id, options);
|
|
53
|
+
});
|
|
54
|
+
specs
|
|
55
|
+
.command('create')
|
|
56
|
+
.description('Create a new specification.')
|
|
57
|
+
.requiredOption('--title <title>', 'Specification title.')
|
|
58
|
+
.option('--project <projectId>', 'Project id (defaults to the one stored via `auth add-token`).')
|
|
59
|
+
.option('--parent <parentId>', 'Parent specification id.')
|
|
60
|
+
.option('--summary <summary>', 'Short summary.')
|
|
61
|
+
.option('--content <content>', 'Full specification content.')
|
|
62
|
+
.option('--status <status>', 'Initial status (draft | approved | processing | done).')
|
|
63
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
await (0, specs_1.specsCreateCommand)(options);
|
|
66
|
+
});
|
|
67
|
+
specs
|
|
68
|
+
.command('update <id>')
|
|
69
|
+
.description('Update fields of an existing specification (only supplied fields are changed).')
|
|
70
|
+
.option('--title <title>', 'New title.')
|
|
71
|
+
.option('--summary <summary>', 'New summary.')
|
|
72
|
+
.option('--clear-summary', 'Clear the summary field.')
|
|
73
|
+
.option('--content <content>', 'New content.')
|
|
74
|
+
.option('--clear-content', 'Clear the content field.')
|
|
75
|
+
.option('--parent <parentId>', 'New parent specification id.')
|
|
76
|
+
.option('--clear-parent', 'Clear the parent specification id.')
|
|
77
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
78
|
+
.action(async (id, options) => {
|
|
79
|
+
await (0, specs_1.specsUpdateCommand)(id, options);
|
|
80
|
+
});
|
|
81
|
+
specs
|
|
82
|
+
.command('set-status <id> <status>')
|
|
83
|
+
.description('Set the status of a specification (draft | approved | processing | done).')
|
|
84
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
85
|
+
.action(async (id, status, options) => {
|
|
86
|
+
await (0, specs_1.specsSetStatusCommand)(id, status, options);
|
|
87
|
+
});
|
|
88
|
+
specs
|
|
89
|
+
.command('set-plan <id>')
|
|
90
|
+
.description('Set (or clear) the implementation plan of a specification.')
|
|
91
|
+
.option('--plan <text>', 'Implementation plan text.')
|
|
92
|
+
.option('--clear-plan', 'Clear the implementation plan.')
|
|
93
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
94
|
+
.action(async (id, options) => {
|
|
95
|
+
await (0, specs_1.specsSetPlanCommand)(id, options);
|
|
96
|
+
});
|
|
97
|
+
specs
|
|
98
|
+
.command('get-plan <id>')
|
|
99
|
+
.description('Get the implementation plan of a specification.')
|
|
100
|
+
.option('--json', 'Print raw JSON instead of plain text.')
|
|
101
|
+
.action(async (id, options) => {
|
|
102
|
+
await (0, specs_1.specsGetPlanCommand)(id, options);
|
|
103
|
+
});
|
|
104
|
+
const specsResults = specs
|
|
105
|
+
.command('results')
|
|
106
|
+
.description('Manage specification execution results via the athenode REST API.');
|
|
107
|
+
specsResults
|
|
108
|
+
.command('list <specificationId>')
|
|
109
|
+
.description('List execution results recorded for a specification.')
|
|
110
|
+
.option('--json', 'Print raw JSON instead of a human-readable table.')
|
|
111
|
+
.action(async (specificationId, options) => {
|
|
112
|
+
await (0, executionResults_1.executionResultsListCommand)(specificationId, options);
|
|
113
|
+
});
|
|
114
|
+
specsResults
|
|
115
|
+
.command('create <specificationId>')
|
|
116
|
+
.description('Record a new execution result for a specification.')
|
|
117
|
+
.requiredOption('--file-path <filePath>', 'Path of the file that was changed.')
|
|
118
|
+
.requiredOption('--summary <summary>', 'Short description of the change.')
|
|
119
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
120
|
+
.action(async (specificationId, options) => {
|
|
121
|
+
await (0, executionResults_1.executionResultsCreateCommand)(specificationId, options);
|
|
122
|
+
});
|
|
123
|
+
const specsQa = specs
|
|
124
|
+
.command('qa')
|
|
125
|
+
.description('Manage specification questions via the athenode REST API.');
|
|
126
|
+
specsQa
|
|
127
|
+
.command('list <specificationId>')
|
|
128
|
+
.description('List questions recorded for a specification.')
|
|
129
|
+
.option('--json', 'Print raw JSON instead of a human-readable table.')
|
|
130
|
+
.action(async (specificationId, options) => {
|
|
131
|
+
await (0, questions_1.questionsListCommand)(specificationId, options);
|
|
132
|
+
});
|
|
133
|
+
specsQa
|
|
134
|
+
.command('add <specificationId>')
|
|
135
|
+
.description('Add a new question for a specification.')
|
|
136
|
+
.requiredOption('--question <question>', 'The question text.')
|
|
137
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
138
|
+
.action(async (specificationId, options) => {
|
|
139
|
+
await (0, questions_1.questionsAddCommand)(specificationId, options);
|
|
140
|
+
});
|
|
141
|
+
specsQa
|
|
142
|
+
.command('answer <specificationId> <questionId>')
|
|
143
|
+
.description('Answer a previously recorded question.')
|
|
144
|
+
.requiredOption('--answer <answer>', 'The answer text.')
|
|
145
|
+
.option('--json', 'Print raw JSON instead of a human-readable block.')
|
|
146
|
+
.action(async (specificationId, questionId, options) => {
|
|
147
|
+
await (0, questions_1.questionsAnswerCommand)(specificationId, questionId, options);
|
|
148
|
+
});
|
|
149
|
+
program.parse(process.argv);
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared output formatting for CLI commands. Every command supports human-readable output by
|
|
4
|
+
* default and a `--json` flag for raw machine-readable JSON, per the CLI's command
|
|
5
|
+
* conventions.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.printJson = printJson;
|
|
9
|
+
exports.formatSpecification = formatSpecification;
|
|
10
|
+
exports.formatSpecificationList = formatSpecificationList;
|
|
11
|
+
exports.formatSpecificationSearchResults = formatSpecificationSearchResults;
|
|
12
|
+
exports.formatExecutionResult = formatExecutionResult;
|
|
13
|
+
exports.formatExecutionResultList = formatExecutionResultList;
|
|
14
|
+
exports.formatQuestion = formatQuestion;
|
|
15
|
+
exports.formatQuestionList = formatQuestionList;
|
|
16
|
+
/** Prints `body` as raw JSON (pretty-printed), used for every command's `--json` mode. */
|
|
17
|
+
function printJson(body) {
|
|
18
|
+
console.log(JSON.stringify(body, null, 2));
|
|
19
|
+
}
|
|
20
|
+
/** Renders a single specification as a human-readable text block. */
|
|
21
|
+
function formatSpecification(spec) {
|
|
22
|
+
const lines = [
|
|
23
|
+
`id: ${spec.id}`,
|
|
24
|
+
`parentId: ${spec.parentId ?? '-'}`,
|
|
25
|
+
`title: ${spec.title}`,
|
|
26
|
+
`status: ${spec.status}`,
|
|
27
|
+
`summary: ${spec.summary ?? '-'}`,
|
|
28
|
+
`hasChildren: ${spec.hasChildren}`,
|
|
29
|
+
];
|
|
30
|
+
if (spec.createdAt) {
|
|
31
|
+
lines.push(`createdAt: ${spec.createdAt}`);
|
|
32
|
+
}
|
|
33
|
+
if (spec.updatedAt) {
|
|
34
|
+
lines.push(`updatedAt: ${spec.updatedAt}`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Renders a list of specifications as a human-readable block: each specification's fields
|
|
40
|
+
* one-per-line, items separated by a `---[spec_id:<id>]---` delimiter.
|
|
41
|
+
*/
|
|
42
|
+
function formatSpecificationList(specs) {
|
|
43
|
+
if (specs.length === 0) {
|
|
44
|
+
return '(no specifications)';
|
|
45
|
+
}
|
|
46
|
+
return specs
|
|
47
|
+
.map((spec) => [
|
|
48
|
+
`---[spec_id:${spec.id}]---`,
|
|
49
|
+
`parentId: ${spec.parentId ?? '-'}`,
|
|
50
|
+
`status: ${spec.status}`,
|
|
51
|
+
`title: ${spec.title}`,
|
|
52
|
+
`summary: ${spec.summary ?? '-'}`,
|
|
53
|
+
`hasChildren: ${spec.hasChildren}`,
|
|
54
|
+
].join('\n'))
|
|
55
|
+
.join('\n');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Renders specification search results as a human-readable block: each result's fields
|
|
59
|
+
* one-per-line, items separated by a `---[spec_id:<id>]---` delimiter (same shape as
|
|
60
|
+
* `formatSpecificationList`, plus `rank`).
|
|
61
|
+
*/
|
|
62
|
+
function formatSpecificationSearchResults(results) {
|
|
63
|
+
if (results.length === 0) {
|
|
64
|
+
return '(no results)';
|
|
65
|
+
}
|
|
66
|
+
return results
|
|
67
|
+
.map((r) => [
|
|
68
|
+
`---[spec_id:${r.id}]---`,
|
|
69
|
+
`parentId: ${r.parentId ?? '-'}`,
|
|
70
|
+
`status: ${r.status}`,
|
|
71
|
+
`title: ${r.title}`,
|
|
72
|
+
`summary: ${r.summary ?? '-'}`,
|
|
73
|
+
`hasChildren: ${r.hasChildren}`,
|
|
74
|
+
`rank: ${r.rank.toFixed(3)}`,
|
|
75
|
+
].join('\n'))
|
|
76
|
+
.join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** Renders a single execution result as a human-readable text block. */
|
|
79
|
+
function formatExecutionResult(result) {
|
|
80
|
+
return [
|
|
81
|
+
`id: ${result.id}`,
|
|
82
|
+
`specificationId: ${result.specificationId}`,
|
|
83
|
+
`filePath: ${result.filePath}`,
|
|
84
|
+
`summary: ${result.summary}`,
|
|
85
|
+
...(result.createdAt ? [`createdAt: ${result.createdAt}`] : []),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Renders a list of execution results as a human-readable block: each result's fields
|
|
90
|
+
* one-per-line, items separated by a `---[result_id:<id>]---` delimiter.
|
|
91
|
+
*/
|
|
92
|
+
function formatExecutionResultList(results) {
|
|
93
|
+
if (results.length === 0) {
|
|
94
|
+
return '(no execution results)';
|
|
95
|
+
}
|
|
96
|
+
return results
|
|
97
|
+
.map((r) => [
|
|
98
|
+
`---[result_id:${r.id}]---`,
|
|
99
|
+
`specificationId: ${r.specificationId}`,
|
|
100
|
+
`filePath: ${r.filePath}`,
|
|
101
|
+
`summary: ${r.summary}`,
|
|
102
|
+
...(r.createdAt ? [`createdAt: ${r.createdAt}`] : []),
|
|
103
|
+
].join('\n'))
|
|
104
|
+
.join('\n');
|
|
105
|
+
}
|
|
106
|
+
/** Renders a single specification question as a human-readable text block. */
|
|
107
|
+
function formatQuestion(question) {
|
|
108
|
+
return [
|
|
109
|
+
`id: ${question.id}`,
|
|
110
|
+
`specificationId: ${question.specificationId}`,
|
|
111
|
+
`question: ${question.question}`,
|
|
112
|
+
`answer: ${question.answer ?? '(unanswered)'}`,
|
|
113
|
+
...(question.createdAt ? [`createdAt: ${question.createdAt}`] : []),
|
|
114
|
+
].join('\n');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Renders a list of specification questions as a human-readable block: each question's fields
|
|
118
|
+
* one-per-line, items separated by a `---[question_id:<id>]---` delimiter. `answer` shows the
|
|
119
|
+
* full recorded answer text, or `(unanswered)` if none has been recorded yet.
|
|
120
|
+
*/
|
|
121
|
+
function formatQuestionList(questions) {
|
|
122
|
+
if (questions.length === 0) {
|
|
123
|
+
return '(no questions)';
|
|
124
|
+
}
|
|
125
|
+
return questions
|
|
126
|
+
.map((q) => [
|
|
127
|
+
`---[question_id:${q.id}]---`,
|
|
128
|
+
`specificationId: ${q.specificationId}`,
|
|
129
|
+
`question: ${q.question}`,
|
|
130
|
+
`answer: ${q.answer ?? '(unanswered)'}`,
|
|
131
|
+
...(q.createdAt ? [`createdAt: ${q.createdAt}`] : []),
|
|
132
|
+
].join('\n'))
|
|
133
|
+
.join('\n');
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@athenode/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Command-line interface for athenode: project scaffold, local config, and token storage.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"athenode": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"packageManager": "npm@11.13.0",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsc --watch"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^0.7.0",
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"tslib": "^2.8.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.10.2",
|
|
27
|
+
"typescript": "~6.0.2"
|
|
28
|
+
}
|
|
29
|
+
}
|