@skhema/cli 0.4.5 → 0.4.7
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 +15 -0
- package/dist/commands/import.d.ts +4 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +238 -0
- package/dist/lib/api/import-transfer.d.ts +22 -0
- package/dist/lib/api/import-transfer.d.ts.map +1 -0
- package/dist/lib/api/import-transfer.js +89 -0
- package/dist/lib/auth/token-store.d.ts.map +1 -1
- package/dist/lib/auth/token-store.js +38 -15
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -57,6 +57,21 @@ skhema api routes # every live route from the Ope
|
|
|
57
57
|
`--data` takes a JSON literal, `@file`, or `-` (stdin). `--query k=v` repeats. The
|
|
58
58
|
passthrough refuses any host other than the Skhema gateway.
|
|
59
59
|
|
|
60
|
+
### Strategy document imports
|
|
61
|
+
|
|
62
|
+
Import a local strategy document or public URL through the same reviewable
|
|
63
|
+
proposal used by the web and MCP surfaces:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
skhema import start --file ./strategy.pdf --wait
|
|
67
|
+
skhema import get imp_... # inspect the mapped proposal
|
|
68
|
+
skhema import decide imp_... --file decisions.json
|
|
69
|
+
skhema import apply imp_... --workspace-name "Imported strategy" --yes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `--json` for a stable machine envelope. Applying or discarding requires an
|
|
73
|
+
interactive confirmation, or an explicit `--yes` in scripts and CI.
|
|
74
|
+
|
|
60
75
|
## Credential lanes
|
|
61
76
|
|
|
62
77
|
The CLI accepts **two kinds** of credential and resolves them in a fixed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAkHnC,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6L7D"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import { resolveClient } from '../lib/api/client.js';
|
|
4
|
+
import { startFileImport, waitForImport } from '../lib/api/import-transfer.js';
|
|
5
|
+
import { parseJsonFile } from '../lib/api/payload.js';
|
|
6
|
+
import { runCommand, UsageError } from '../lib/api/run-command.js';
|
|
7
|
+
import { getGlobalOptions, log, logHeader, logTable } from '../lib/output.js';
|
|
8
|
+
function printSession(session) {
|
|
9
|
+
logHeader(session.proposalPayload?.summary.title ?? `Import ${session.id}`);
|
|
10
|
+
log(` Id: ${chalk.cyan(session.id)}`);
|
|
11
|
+
log(` Status: ${session.status}`);
|
|
12
|
+
const source = session.sourceFilename ?? session.sourceUrl;
|
|
13
|
+
if (source)
|
|
14
|
+
log(` Source: ${source}`);
|
|
15
|
+
if (session.progressStage) {
|
|
16
|
+
const count = session.progressCurrent !== null && session.progressTotal !== null
|
|
17
|
+
? ` ${session.progressCurrent}/${session.progressTotal}`
|
|
18
|
+
: '';
|
|
19
|
+
log(` Progress: ${session.progressStage}${count}`);
|
|
20
|
+
}
|
|
21
|
+
if (session.error)
|
|
22
|
+
log(` Error: ${session.error}`);
|
|
23
|
+
if (session.workspaceId)
|
|
24
|
+
log(` Workspace: ${session.workspaceId}`);
|
|
25
|
+
const proposal = session.proposalPayload;
|
|
26
|
+
if (!proposal)
|
|
27
|
+
return;
|
|
28
|
+
log('');
|
|
29
|
+
log(`${proposal.items.length} mapped item${proposal.items.length === 1 ? '' : 's'}, ` +
|
|
30
|
+
`${proposal.orphans.length} unmapped.`);
|
|
31
|
+
if (proposal.items.length > 0) {
|
|
32
|
+
logTable(['Id', 'Component', 'Type', 'Confidence', 'Content'], proposal.items.map((item) => [
|
|
33
|
+
item.id,
|
|
34
|
+
item.componentType,
|
|
35
|
+
item.elementType,
|
|
36
|
+
item.confidence,
|
|
37
|
+
item.proposed.content.length > 60
|
|
38
|
+
? `${item.proposed.content.slice(0, 57)}...`
|
|
39
|
+
: item.proposed.content,
|
|
40
|
+
]));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function organizationFor(explicit, credential) {
|
|
44
|
+
const organizationId = explicit ?? credential.organizationId;
|
|
45
|
+
if (credential.kind === 'oauth' && !organizationId) {
|
|
46
|
+
throw new UsageError('No organization is available for this OAuth session. Pass --organization <id>.');
|
|
47
|
+
}
|
|
48
|
+
return organizationId;
|
|
49
|
+
}
|
|
50
|
+
function interactiveTerminal() {
|
|
51
|
+
return (Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
|
|
52
|
+
!getGlobalOptions().json);
|
|
53
|
+
}
|
|
54
|
+
async function confirm(question) {
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
});
|
|
59
|
+
const answer = await new Promise((resolve) => {
|
|
60
|
+
rl.question(`${question} [y/N]: `, (value) => {
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(value.trim().toLowerCase());
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return answer === 'y' || answer === 'yes';
|
|
66
|
+
}
|
|
67
|
+
async function authorizeMutation(question, yes) {
|
|
68
|
+
if (yes)
|
|
69
|
+
return true;
|
|
70
|
+
if (!interactiveTerminal()) {
|
|
71
|
+
throw new UsageError('This command requires --yes when run non-interactively.');
|
|
72
|
+
}
|
|
73
|
+
return confirm(question);
|
|
74
|
+
}
|
|
75
|
+
function decisionsFrom(source) {
|
|
76
|
+
const parsed = parseJsonFile(source);
|
|
77
|
+
const candidate = parsed.decisions &&
|
|
78
|
+
typeof parsed.decisions === 'object' &&
|
|
79
|
+
!Array.isArray(parsed.decisions)
|
|
80
|
+
? parsed.decisions
|
|
81
|
+
: parsed;
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
async function maybeWait(client, response, wait) {
|
|
85
|
+
if (!wait)
|
|
86
|
+
return response;
|
|
87
|
+
return waitForImport({ client, sessionId: response.session.id });
|
|
88
|
+
}
|
|
89
|
+
/** Register the complete proposal-first import session lifecycle. */
|
|
90
|
+
export function registerImportCommands(program) {
|
|
91
|
+
const imports = program
|
|
92
|
+
.command('import')
|
|
93
|
+
.description('Import a strategy document through a reviewable proposal');
|
|
94
|
+
imports
|
|
95
|
+
.command('start')
|
|
96
|
+
.description('Start an import from a local file or public URL')
|
|
97
|
+
.option('--file <path>', 'Local PDF, DOCX, Markdown, text, CSV, XLSX, or JSON')
|
|
98
|
+
.option('--url <url>', 'Public http(s) URL to import')
|
|
99
|
+
.option('--organization <id>', 'Organization id (OAuth user lane)')
|
|
100
|
+
.option('--wait', 'Poll until the proposal is ready or analysis fails')
|
|
101
|
+
.action(async (options) => {
|
|
102
|
+
await runCommand('import start', async () => {
|
|
103
|
+
if (Boolean(options.file) === Boolean(options.url)) {
|
|
104
|
+
throw new UsageError('Pass exactly one of --file <path> or --url <url>.');
|
|
105
|
+
}
|
|
106
|
+
if (options.url && !/^https?:\/\//i.test(options.url)) {
|
|
107
|
+
throw new UsageError('--url must be an http(s) URL.');
|
|
108
|
+
}
|
|
109
|
+
const { client, credential } = await resolveClient();
|
|
110
|
+
const organizationId = organizationFor(options.organization, credential);
|
|
111
|
+
let response = options.file
|
|
112
|
+
? await startFileImport({
|
|
113
|
+
client,
|
|
114
|
+
filePath: options.file,
|
|
115
|
+
organizationId,
|
|
116
|
+
})
|
|
117
|
+
: await client.imports.create({
|
|
118
|
+
sourceType: 'url',
|
|
119
|
+
...(organizationId ? { organizationId } : {}),
|
|
120
|
+
url: options.url,
|
|
121
|
+
});
|
|
122
|
+
response = await maybeWait(client, response, options.wait);
|
|
123
|
+
if (!getGlobalOptions().json)
|
|
124
|
+
printSession(response.session);
|
|
125
|
+
return response;
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
imports
|
|
129
|
+
.command('list')
|
|
130
|
+
.description('List recent import sessions')
|
|
131
|
+
.option('--organization <id>', 'Organization id (OAuth user lane)')
|
|
132
|
+
.action(async (options) => {
|
|
133
|
+
await runCommand('import list', async () => {
|
|
134
|
+
const { client, credential } = await resolveClient();
|
|
135
|
+
const organizationId = organizationFor(options.organization, credential);
|
|
136
|
+
const response = await client.imports.list(organizationId);
|
|
137
|
+
if (!getGlobalOptions().json) {
|
|
138
|
+
logHeader(`Imports (${response.sessions.length})`);
|
|
139
|
+
if (response.sessions.length === 0)
|
|
140
|
+
log(chalk.dim(' No imports.'));
|
|
141
|
+
else
|
|
142
|
+
logTable(['Status', 'Source', 'Updated', 'Id'], response.sessions.map((session) => [
|
|
143
|
+
session.status,
|
|
144
|
+
session.sourceFilename ?? session.sourceUrl ?? '—',
|
|
145
|
+
session.updatedAt,
|
|
146
|
+
session.id,
|
|
147
|
+
]));
|
|
148
|
+
}
|
|
149
|
+
return response;
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
imports
|
|
153
|
+
.command('get <session-id>')
|
|
154
|
+
.description('Inspect an import session and its proposal')
|
|
155
|
+
.option('--wait', 'Poll until the proposal is ready or analysis fails')
|
|
156
|
+
.action(async (sessionId, options) => {
|
|
157
|
+
await runCommand('import get', async () => {
|
|
158
|
+
const { client } = await resolveClient();
|
|
159
|
+
let response = await client.imports.get(sessionId);
|
|
160
|
+
response = await maybeWait(client, response, options.wait);
|
|
161
|
+
if (!getGlobalOptions().json)
|
|
162
|
+
printSession(response.session);
|
|
163
|
+
return response;
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
imports
|
|
167
|
+
.command('decide <session-id>')
|
|
168
|
+
.description('Replace proposal decisions from a JSON object')
|
|
169
|
+
.requiredOption('--file <path|->', 'Decisions JSON file, or - for stdin')
|
|
170
|
+
.action(async (sessionId, options) => {
|
|
171
|
+
await runCommand('import decide', async () => {
|
|
172
|
+
const { client } = await resolveClient();
|
|
173
|
+
const response = await client.imports.updateDecisions(sessionId, decisionsFrom(options.file));
|
|
174
|
+
if (!getGlobalOptions().json) {
|
|
175
|
+
log(chalk.green('Import decisions saved.'));
|
|
176
|
+
printSession(response.session);
|
|
177
|
+
}
|
|
178
|
+
return response;
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
imports
|
|
182
|
+
.command('apply <session-id>')
|
|
183
|
+
.description('Apply a reviewed proposal into a new workspace')
|
|
184
|
+
.requiredOption('--workspace-name <name>', 'Name for the new workspace')
|
|
185
|
+
.option('--visibility <visibility>', 'global | restricted', 'global')
|
|
186
|
+
.option('--yes', 'Confirm without prompting (required when non-interactive)')
|
|
187
|
+
.action(async (sessionId, options) => {
|
|
188
|
+
await runCommand('import apply', async () => {
|
|
189
|
+
if (options.visibility !== 'global' &&
|
|
190
|
+
options.visibility !== 'restricted') {
|
|
191
|
+
throw new UsageError('--visibility must be global or restricted.');
|
|
192
|
+
}
|
|
193
|
+
const authorized = await authorizeMutation(`Apply import ${sessionId} and create workspace "${options.workspaceName}"?`, options.yes ?? false);
|
|
194
|
+
if (!authorized)
|
|
195
|
+
return { cancelled: true, sessionId };
|
|
196
|
+
const { client } = await resolveClient();
|
|
197
|
+
const response = await client.imports.apply(sessionId, {
|
|
198
|
+
workspaceName: options.workspaceName,
|
|
199
|
+
visibility: options.visibility,
|
|
200
|
+
});
|
|
201
|
+
if (!getGlobalOptions().json) {
|
|
202
|
+
log(chalk.green('Import applied.'));
|
|
203
|
+
printSession(response.session);
|
|
204
|
+
}
|
|
205
|
+
return response;
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
imports
|
|
209
|
+
.command('discard <session-id>')
|
|
210
|
+
.description('Discard an eligible import session')
|
|
211
|
+
.option('--yes', 'Confirm without prompting (required when non-interactive)')
|
|
212
|
+
.action(async (sessionId, options) => {
|
|
213
|
+
await runCommand('import discard', async () => {
|
|
214
|
+
const authorized = await authorizeMutation(`Discard import ${sessionId}?`, options.yes ?? false);
|
|
215
|
+
if (!authorized)
|
|
216
|
+
return { cancelled: true, sessionId };
|
|
217
|
+
const { client } = await resolveClient();
|
|
218
|
+
const response = await client.imports.discard(sessionId);
|
|
219
|
+
if (!getGlobalOptions().json)
|
|
220
|
+
log(chalk.green('Import discarded.'));
|
|
221
|
+
return response;
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
imports
|
|
225
|
+
.command('retry <session-id>')
|
|
226
|
+
.description('Retry a failed or stale import analysis')
|
|
227
|
+
.option('--wait', 'Poll until the proposal is ready or analysis fails')
|
|
228
|
+
.action(async (sessionId, options) => {
|
|
229
|
+
await runCommand('import retry', async () => {
|
|
230
|
+
const { client } = await resolveClient();
|
|
231
|
+
let response = await client.imports.retry(sessionId);
|
|
232
|
+
response = await maybeWait(client, response, options.wait);
|
|
233
|
+
if (!getGlobalOptions().json)
|
|
234
|
+
printSession(response.session);
|
|
235
|
+
return response;
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ImportSessionResponse, SkhemaClient } from '@skhema/sdk';
|
|
2
|
+
/** Resolve the backend-supported import MIME for a local file. */
|
|
3
|
+
export declare function importMimeFor(filePath: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Start a file import, stream the bytes to its signed URL, and confirm upload.
|
|
6
|
+
* The file is never buffered in memory.
|
|
7
|
+
*/
|
|
8
|
+
export declare function startFileImport(options: {
|
|
9
|
+
client: SkhemaClient;
|
|
10
|
+
filePath: string;
|
|
11
|
+
organizationId?: string;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
}): Promise<ImportSessionResponse>;
|
|
14
|
+
/** Poll the canonical session until it reaches a reviewable or terminal state. */
|
|
15
|
+
export declare function waitForImport(options: {
|
|
16
|
+
client: SkhemaClient;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
intervalMs?: number;
|
|
19
|
+
maxWaitMs?: number;
|
|
20
|
+
sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
}): Promise<ImportSessionResponse>;
|
|
22
|
+
//# sourceMappingURL=import-transfer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-transfer.d.ts","sourceRoot":"","sources":["../../../src/lib/api/import-transfer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBtE,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStD;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4CjC;AAUD,kFAAkF;AAClF,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAkBjC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
import { UsageError } from './run-command.js';
|
|
5
|
+
const IMPORT_MIME_BY_EXTENSION = {
|
|
6
|
+
pdf: 'application/pdf',
|
|
7
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
8
|
+
md: 'text/markdown',
|
|
9
|
+
txt: 'text/plain',
|
|
10
|
+
csv: 'text/csv',
|
|
11
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
12
|
+
json: 'application/json',
|
|
13
|
+
};
|
|
14
|
+
/** Resolve the backend-supported import MIME for a local file. */
|
|
15
|
+
export function importMimeFor(filePath) {
|
|
16
|
+
const extension = path.extname(filePath).slice(1).toLowerCase();
|
|
17
|
+
const mime = IMPORT_MIME_BY_EXTENSION[extension];
|
|
18
|
+
if (!mime) {
|
|
19
|
+
throw new UsageError('Unsupported import file. Use PDF, DOCX, Markdown, text, CSV, XLSX, or JSON.');
|
|
20
|
+
}
|
|
21
|
+
return mime;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start a file import, stream the bytes to its signed URL, and confirm upload.
|
|
25
|
+
* The file is never buffered in memory.
|
|
26
|
+
*/
|
|
27
|
+
export async function startFileImport(options) {
|
|
28
|
+
let sizeBytes;
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(options.filePath);
|
|
31
|
+
if (!stat.isFile())
|
|
32
|
+
throw new Error('not a file');
|
|
33
|
+
sizeBytes = stat.size;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new UsageError(`Cannot read import file: ${options.filePath}`);
|
|
37
|
+
}
|
|
38
|
+
const filename = path.basename(options.filePath);
|
|
39
|
+
const mime = importMimeFor(filename);
|
|
40
|
+
const created = await options.client.imports.create({
|
|
41
|
+
sourceType: 'file',
|
|
42
|
+
...(options.organizationId
|
|
43
|
+
? { organizationId: options.organizationId }
|
|
44
|
+
: {}),
|
|
45
|
+
filename,
|
|
46
|
+
mime,
|
|
47
|
+
sizeBytes,
|
|
48
|
+
});
|
|
49
|
+
if (!created.uploadUrl) {
|
|
50
|
+
throw new Error('The import service did not return an upload URL.');
|
|
51
|
+
}
|
|
52
|
+
const response = await (options.fetchImpl ?? globalThis.fetch)(created.uploadUrl, {
|
|
53
|
+
method: 'PUT',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': mime,
|
|
56
|
+
'Content-Length': String(sizeBytes),
|
|
57
|
+
},
|
|
58
|
+
body: Readable.toWeb(fs.createReadStream(options.filePath)),
|
|
59
|
+
duplex: 'half',
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`Uploading import file failed (HTTP ${response.status}).`);
|
|
63
|
+
}
|
|
64
|
+
return options.client.imports.confirmUpload(created.session.id);
|
|
65
|
+
}
|
|
66
|
+
const TERMINAL_IMPORT_STATUSES = new Set([
|
|
67
|
+
'proposed',
|
|
68
|
+
'applied',
|
|
69
|
+
'partially_applied',
|
|
70
|
+
'failed',
|
|
71
|
+
'discarded',
|
|
72
|
+
]);
|
|
73
|
+
/** Poll the canonical session until it reaches a reviewable or terminal state. */
|
|
74
|
+
export async function waitForImport(options) {
|
|
75
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
76
|
+
const maxWaitMs = options.maxWaitMs ?? 30 * 60000;
|
|
77
|
+
const sleep = options.sleep ??
|
|
78
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
while (true) {
|
|
81
|
+
const response = await options.client.imports.get(options.sessionId);
|
|
82
|
+
if (TERMINAL_IMPORT_STATUSES.has(response.session.status))
|
|
83
|
+
return response;
|
|
84
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
85
|
+
throw new Error(`Timed out waiting for import ${options.sessionId}; inspect it with "skhema import get ${options.sessionId}".`);
|
|
86
|
+
}
|
|
87
|
+
await sleep(intervalMs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/token-store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAoFnD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/token-store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAoFnD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA6D9E;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGtD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
|
|
@@ -90,28 +90,51 @@ export async function getStoredCredentials() {
|
|
|
90
90
|
const credentials = JSON.parse(data);
|
|
91
91
|
// Auto-refresh if within buffer of expiry
|
|
92
92
|
if (credentials.expires_at - Date.now() < TOKEN_REFRESH_BUFFER) {
|
|
93
|
+
let newTokens;
|
|
94
|
+
try {
|
|
95
|
+
newTokens = await refreshAccessToken(credentials.refresh_token);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Refresh failed. If the access token is still valid (refresh was just
|
|
99
|
+
// the pre-expiry buffer, e.g. a network blip), the credentials remain
|
|
100
|
+
// usable. If it has ALREADY expired, the session is dead (refresh
|
|
101
|
+
// tokens live 7 days) — returning it would present an unusable bearer
|
|
102
|
+
// downstream as a "valid session" and surface as a bare 401 far from
|
|
103
|
+
// the cause. Treat as unauthenticated instead.
|
|
104
|
+
if (credentials.expires_at > Date.now()) {
|
|
105
|
+
return credentials;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Refresh succeeded — the session is alive. Nothing past this point is
|
|
110
|
+
// allowed to discard it: userinfo is cosmetic (keep the prior identity
|
|
111
|
+
// on failure) and a persist failure still leaves the tokens usable for
|
|
112
|
+
// this invocation.
|
|
113
|
+
const refreshed = {
|
|
114
|
+
...credentials,
|
|
115
|
+
access_token: newTokens.access_token,
|
|
116
|
+
refresh_token: newTokens.refresh_token,
|
|
117
|
+
expires_at: Date.now() + newTokens.expires_in * 1000,
|
|
118
|
+
scope: newTokens.scope,
|
|
119
|
+
};
|
|
93
120
|
try {
|
|
94
|
-
const newTokens = await refreshAccessToken(credentials.refresh_token);
|
|
95
121
|
const userInfo = await fetchUserInfo(newTokens.access_token);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expires_at: Date.now() + newTokens.expires_in * 1000,
|
|
101
|
-
scope: newTokens.scope,
|
|
102
|
-
user: {
|
|
103
|
-
id: userInfo.sub,
|
|
104
|
-
email: userInfo.email,
|
|
105
|
-
name: userInfo.name,
|
|
106
|
-
},
|
|
122
|
+
refreshed.user = {
|
|
123
|
+
id: userInfo.sub,
|
|
124
|
+
email: userInfo.email,
|
|
125
|
+
name: userInfo.name,
|
|
107
126
|
};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* keep the previously stored user */
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
108
132
|
await storeCredentials(refreshed);
|
|
109
|
-
return refreshed;
|
|
110
133
|
}
|
|
111
134
|
catch {
|
|
112
|
-
|
|
113
|
-
return credentials;
|
|
135
|
+
/* still return the in-memory refreshed session */
|
|
114
136
|
}
|
|
137
|
+
return refreshed;
|
|
115
138
|
}
|
|
116
139
|
return credentials;
|
|
117
140
|
}
|
package/dist/program.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiDrD"}
|
package/dist/program.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerContributeCommands } from './commands/contribute.js';
|
|
|
7
7
|
import { registerDocumentCommands } from './commands/document.js';
|
|
8
8
|
import { registerElementCommands } from './commands/element.js';
|
|
9
9
|
import { registerWorkspaceExportCommand } from './commands/export.js';
|
|
10
|
+
import { registerImportCommands } from './commands/import.js';
|
|
10
11
|
import { registerInitCommand } from './commands/init.js';
|
|
11
12
|
import { registerLinkCommands } from './commands/link.js';
|
|
12
13
|
import { registerResourceCommands } from './commands/resource.js';
|
|
@@ -47,6 +48,7 @@ export function buildProgram(version) {
|
|
|
47
48
|
program.help();
|
|
48
49
|
});
|
|
49
50
|
registerInitCommand(program);
|
|
51
|
+
registerImportCommands(program);
|
|
50
52
|
registerAuthCommands(program);
|
|
51
53
|
registerWorkspaceCommands(program);
|
|
52
54
|
registerElementCommands(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skhema/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Skhema CLI - Authentication and AI skills management for agent platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@skhema/agent-sdk": "0.1.4",
|
|
45
45
|
"@skhema/method": "0.3.0",
|
|
46
|
-
"@skhema/sdk": "0.2.
|
|
46
|
+
"@skhema/sdk": "0.2.3",
|
|
47
47
|
"chalk": "^5.3.0",
|
|
48
48
|
"commander": "^12.0.0",
|
|
49
49
|
"ora": "^8.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@mdx-js/mdx": "3.1.1",
|
|
53
|
-
"@skhema/linter": "2.
|
|
53
|
+
"@skhema/linter": "2.3.0",
|
|
54
54
|
"@skhema/prettier-config": "1.0.0",
|
|
55
55
|
"@types/node": "^22.0.0",
|
|
56
56
|
"eslint": "^9.0.0",
|