@rover-studio/answer-me 0.1.0-rc.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/bin/answerme-toolkit.mjs +6 -0
- package/distribution/npm/migrations.json +605 -0
- package/distribution/npm/package-manifest.json +192 -0
- package/distribution/npm/skills/answerme/SKILL.md +94 -0
- package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
- package/distribution/npm/skills/answerme/references/api.md +135 -0
- package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
- package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
- package/distribution/npm/skills/answerme/references/errors.md +44 -0
- package/distribution/npm/skills/answerme/references/handoff.md +46 -0
- package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
- package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
- package/distribution/npm/skills/answerme/references/templates.md +139 -0
- package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
- package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
- package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
- package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
- package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
- package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
- package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
- package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
- package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
- package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
- package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
- package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
- package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
- package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
- package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
- package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
- package/lib/npm-cli/commands.mjs +247 -0
- package/lib/npm-cli/constants.mjs +51 -0
- package/lib/npm-cli/errors.mjs +15 -0
- package/lib/npm-cli/filesystem.mjs +193 -0
- package/lib/npm-cli/host-discovery.mjs +404 -0
- package/lib/npm-cli/main.mjs +42 -0
- package/lib/npm-cli/package-integrity.mjs +212 -0
- package/lib/npm-cli/transaction.mjs +375 -0
- package/lib/npm-cli/usage-validation.mjs +349 -0
- package/package.json +17 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
MAX_BUFFER,
|
|
6
|
+
OFFICIAL_API_ROOT,
|
|
7
|
+
USAGE_VALIDATION_TIMEOUT_SECONDS,
|
|
8
|
+
} from './constants.mjs';
|
|
9
|
+
import { normalizePath, pathExists } from './filesystem.mjs';
|
|
10
|
+
|
|
11
|
+
const ENROLLMENT_EVENT = 'answerme.creator-enrollment.verification-url';
|
|
12
|
+
const ENROLLMENT_STAGE = 'verification-handoff';
|
|
13
|
+
const MAX_ENROLLMENT_EVENT_BYTES = 4096;
|
|
14
|
+
const SELF_TEST_OUTPUT_KEYS = Object.freeze([
|
|
15
|
+
'cleanupWarning',
|
|
16
|
+
'code',
|
|
17
|
+
'message',
|
|
18
|
+
'networkCalled',
|
|
19
|
+
'ok',
|
|
20
|
+
'resultTokenCleanup',
|
|
21
|
+
'stage',
|
|
22
|
+
'status',
|
|
23
|
+
]);
|
|
24
|
+
const SELF_TEST_ISSUES = Object.freeze({
|
|
25
|
+
'self-test-dependency-missing': Object.freeze({
|
|
26
|
+
stage: 'preflight',
|
|
27
|
+
message: 'A required AnswerMe self-test adapter is missing.',
|
|
28
|
+
}),
|
|
29
|
+
'self-test-resume-unsupported': Object.freeze({
|
|
30
|
+
stage: 'preflight',
|
|
31
|
+
message: 'Installation usage validation must create and wait for its own fixed questionnaire.',
|
|
32
|
+
}),
|
|
33
|
+
'self-test-create-stderr-invalid': Object.freeze({
|
|
34
|
+
stage: 'create',
|
|
35
|
+
message: 'The AnswerMe create adapter emitted an untrusted stderr record.',
|
|
36
|
+
}),
|
|
37
|
+
'self-test-create-validation-failed': Object.freeze({
|
|
38
|
+
stage: 'create',
|
|
39
|
+
message: 'The AnswerMe self-test questionnaire contract could not be validated.',
|
|
40
|
+
}),
|
|
41
|
+
'self-test-create-failed': Object.freeze({
|
|
42
|
+
stage: 'create',
|
|
43
|
+
message: 'The AnswerMe self-test questionnaire could not be created and verified.',
|
|
44
|
+
}),
|
|
45
|
+
'self-test-wait-failed': Object.freeze({
|
|
46
|
+
stage: 'wait',
|
|
47
|
+
message: 'The AnswerMe self-test did not reach a readable terminal result.',
|
|
48
|
+
}),
|
|
49
|
+
'self-test-result-invalid': Object.freeze({
|
|
50
|
+
stage: 'mapping',
|
|
51
|
+
message: 'The AnswerMe self-test result did not match the fixed three-question contract.',
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
const CLEANUP_WARNING = Object.freeze({
|
|
55
|
+
code: 'self-test-cleanup-failed',
|
|
56
|
+
message: 'The protected self-test Result Token could not be confirmed removed.',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function isExactOfficialApiRoot(value = OFFICIAL_API_ROOT) {
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
return url.protocol === 'https:'
|
|
63
|
+
&& url.origin === value
|
|
64
|
+
&& url.pathname === '/'
|
|
65
|
+
&& !url.username
|
|
66
|
+
&& !url.password
|
|
67
|
+
&& !url.search
|
|
68
|
+
&& !url.hash;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isExactOfficialApiRoot()) throw new Error('invalid official AnswerMe API root');
|
|
75
|
+
|
|
76
|
+
function isRecord(value) {
|
|
77
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasExactKeys(value, keys) {
|
|
81
|
+
return isRecord(value)
|
|
82
|
+
&& JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function exactCleanup(value, attempted, removed) {
|
|
86
|
+
return hasExactKeys(value, ['attempted', 'removed'])
|
|
87
|
+
&& value.attempted === attempted
|
|
88
|
+
&& value.removed === removed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function exactCleanupWarning(value) {
|
|
92
|
+
return hasExactKeys(value, ['code', 'message'])
|
|
93
|
+
&& value.code === CLEANUP_WARNING.code
|
|
94
|
+
&& value.message === CLEANUP_WARNING.message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateSelfTestOutput(value) {
|
|
98
|
+
if (!hasExactKeys(value, SELF_TEST_OUTPUT_KEYS) || typeof value.networkCalled !== 'boolean') return false;
|
|
99
|
+
if (value.ok === true
|
|
100
|
+
&& value.status === 'passed'
|
|
101
|
+
&& value.stage === 'complete'
|
|
102
|
+
&& value.code === null
|
|
103
|
+
&& value.message === null
|
|
104
|
+
&& value.networkCalled === true
|
|
105
|
+
&& (exactCleanup(value.resultTokenCleanup, true, true)
|
|
106
|
+
|| exactCleanup(value.resultTokenCleanup, true, false))) {
|
|
107
|
+
return value.resultTokenCleanup.removed === true
|
|
108
|
+
? value.cleanupWarning === null
|
|
109
|
+
: value.resultTokenCleanup.removed === false && exactCleanupWarning(value.cleanupWarning);
|
|
110
|
+
}
|
|
111
|
+
const issue = typeof value.code === 'string' ? SELF_TEST_ISSUES[value.code] : null;
|
|
112
|
+
return value.ok === false
|
|
113
|
+
&& value.status === 'issue'
|
|
114
|
+
&& issue !== null
|
|
115
|
+
&& value.stage === issue.stage
|
|
116
|
+
&& value.message === issue.message
|
|
117
|
+
&& exactCleanup(value.resultTokenCleanup, false, false)
|
|
118
|
+
&& value.cleanupWarning === null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseSingleJson(stdout) {
|
|
122
|
+
const lines = String(stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
123
|
+
if (lines.length !== 1 || Buffer.byteLength(lines[0], 'utf8') > MAX_BUFFER) return null;
|
|
124
|
+
try {
|
|
125
|
+
const value = JSON.parse(lines[0]);
|
|
126
|
+
return isRecord(value) ? value : null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseEnrollmentEvent(line) {
|
|
133
|
+
const text = String(line ?? '').trim();
|
|
134
|
+
if (!text || Buffer.byteLength(text, 'utf8') > MAX_ENROLLMENT_EVENT_BYTES) return null;
|
|
135
|
+
let value;
|
|
136
|
+
try {
|
|
137
|
+
value = JSON.parse(text);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (!isRecord(value)
|
|
142
|
+
|| JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(['event', 'stage', 'verificationUrl'])
|
|
143
|
+
|| value.event !== ENROLLMENT_EVENT
|
|
144
|
+
|| value.stage !== ENROLLMENT_STAGE
|
|
145
|
+
|| typeof value.verificationUrl !== 'string') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
let verificationUrl;
|
|
149
|
+
try {
|
|
150
|
+
verificationUrl = new URL(value.verificationUrl);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (verificationUrl.protocol !== 'https:'
|
|
155
|
+
|| verificationUrl.origin !== OFFICIAL_API_ROOT
|
|
156
|
+
|| verificationUrl.username
|
|
157
|
+
|| verificationUrl.password
|
|
158
|
+
|| verificationUrl.hash) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return JSON.stringify({
|
|
162
|
+
event: ENROLLMENT_EVENT,
|
|
163
|
+
stage: ENROLLMENT_STAGE,
|
|
164
|
+
verificationUrl: verificationUrl.href,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function safeProcessError(code) {
|
|
169
|
+
return Object.assign(new Error(code), { code });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function invokeValidationProcess(executable, args, options, dependencies = {}) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const spawn = dependencies.spawn ?? nodeSpawn;
|
|
175
|
+
const stderrWriter = dependencies.stderrWriter ?? ((text) => process.stderr.write(text));
|
|
176
|
+
let child;
|
|
177
|
+
try {
|
|
178
|
+
child = spawn(executable, args, {
|
|
179
|
+
shell: false,
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
env: options.env,
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
});
|
|
184
|
+
} catch {
|
|
185
|
+
reject(safeProcessError('usage-validation-process-start-failed'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!child?.stdout || !child?.stderr || typeof child.kill !== 'function') {
|
|
189
|
+
try { child?.kill?.(); } catch { /* the process handle is already invalid */ }
|
|
190
|
+
reject(safeProcessError('usage-validation-process-invalid'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let settled = false;
|
|
195
|
+
let stdout = '';
|
|
196
|
+
let stdoutBytes = 0;
|
|
197
|
+
let stderrPending = '';
|
|
198
|
+
let stderrBytes = 0;
|
|
199
|
+
let enrollmentEventCount = 0;
|
|
200
|
+
let failureCode = null;
|
|
201
|
+
let timedOut = false;
|
|
202
|
+
|
|
203
|
+
const stop = (code) => {
|
|
204
|
+
if (!failureCode) failureCode = code;
|
|
205
|
+
try { child.kill(); } catch { /* close/error settles the result */ }
|
|
206
|
+
};
|
|
207
|
+
const consumeStderrLine = (line) => {
|
|
208
|
+
if (!line) {
|
|
209
|
+
stop('usage-validation-stderr-invalid');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const safeEvent = parseEnrollmentEvent(line);
|
|
213
|
+
if (!safeEvent || enrollmentEventCount !== 0) {
|
|
214
|
+
stop('usage-validation-stderr-invalid');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
enrollmentEventCount += 1;
|
|
218
|
+
try {
|
|
219
|
+
stderrWriter(`${safeEvent}\n`);
|
|
220
|
+
} catch {
|
|
221
|
+
stop('usage-validation-stderr-relay-failed');
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const timer = setTimeout(() => {
|
|
225
|
+
timedOut = true;
|
|
226
|
+
stop('ETIMEDOUT');
|
|
227
|
+
}, options.timeoutMs);
|
|
228
|
+
const finish = (error, value) => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
if (error) reject(error);
|
|
233
|
+
else resolve(value);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
child.stdout.setEncoding('utf8');
|
|
237
|
+
child.stderr.setEncoding('utf8');
|
|
238
|
+
child.stdout.on('data', (chunk) => {
|
|
239
|
+
stdoutBytes += Buffer.byteLength(chunk, 'utf8');
|
|
240
|
+
if (stdoutBytes > options.maxBuffer) {
|
|
241
|
+
stop('usage-validation-stdout-too-large');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
stdout += chunk;
|
|
245
|
+
});
|
|
246
|
+
child.stderr.on('data', (chunk) => {
|
|
247
|
+
stderrBytes += Buffer.byteLength(chunk, 'utf8');
|
|
248
|
+
if (stderrBytes > options.maxBuffer) {
|
|
249
|
+
stop('usage-validation-stderr-too-large');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
stderrPending += chunk;
|
|
253
|
+
let newline;
|
|
254
|
+
while ((newline = stderrPending.indexOf('\n')) !== -1) {
|
|
255
|
+
const line = stderrPending.slice(0, newline).replace(/\r$/, '');
|
|
256
|
+
stderrPending = stderrPending.slice(newline + 1);
|
|
257
|
+
consumeStderrLine(line);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
child.stdout.once('error', () => stop('usage-validation-stdout-read-failed'));
|
|
261
|
+
child.stderr.once('error', () => stop('usage-validation-stderr-read-failed'));
|
|
262
|
+
child.once('error', () => finish(safeProcessError('usage-validation-process-error')));
|
|
263
|
+
child.once('close', (code, signal) => {
|
|
264
|
+
if (stderrPending) stop('usage-validation-stderr-invalid');
|
|
265
|
+
if (timedOut) {
|
|
266
|
+
finish(safeProcessError('ETIMEDOUT'));
|
|
267
|
+
} else if (failureCode) {
|
|
268
|
+
finish(safeProcessError(failureCode));
|
|
269
|
+
} else if (code !== 0 || signal) {
|
|
270
|
+
finish(safeProcessError('usage-validation-process-exit'));
|
|
271
|
+
} else {
|
|
272
|
+
finish(null, { stdout });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function validatePowerShell(candidate, dependencies) {
|
|
279
|
+
if (typeof candidate !== 'string' || !path.isAbsolute(candidate) || path.basename(candidate).toLowerCase() !== 'powershell.exe') {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (typeof dependencies.inspectPowerShell === 'function') return dependencies.inspectPowerShell(candidate);
|
|
283
|
+
const info = await lstat(candidate).catch(() => null);
|
|
284
|
+
if (!info?.isFile() || info.isSymbolicLink()) return false;
|
|
285
|
+
return normalizePath(await realpath(candidate)) === normalizePath(candidate);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function result(status, code, cleanupWarning = null) {
|
|
289
|
+
return { status, code, cleanupWarning };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function windowsPowerShellEnvironment(powershellExecutable, source = process.env) {
|
|
293
|
+
const environment = {};
|
|
294
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
295
|
+
if (key.toLowerCase() !== 'psmodulepath' && value !== undefined) environment[key] = value;
|
|
296
|
+
}
|
|
297
|
+
environment.PSModulePath = path.join(path.dirname(powershellExecutable), 'Modules');
|
|
298
|
+
return environment;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function runUsageValidation({ skillsRoot, timeoutSeconds = USAGE_VALIDATION_TIMEOUT_SECONDS }, dependencies = {}) {
|
|
302
|
+
const script = path.join(skillsRoot, 'answerme', 'scripts', 'test-answerme-installation.ps1');
|
|
303
|
+
const powershell = dependencies.powershellExecutable
|
|
304
|
+
?? path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
305
|
+
if (timeoutSeconds !== USAGE_VALIDATION_TIMEOUT_SECONDS
|
|
306
|
+
|| !(await pathExists(script))
|
|
307
|
+
|| !(await validatePowerShell(powershell, dependencies))) {
|
|
308
|
+
return result('issue', 'usage-validation-runner-unavailable');
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const runner = dependencies.invokeProcess ?? invokeValidationProcess;
|
|
312
|
+
const executed = await runner(powershell, [
|
|
313
|
+
'-NoLogo',
|
|
314
|
+
'-NoProfile',
|
|
315
|
+
'-NonInteractive',
|
|
316
|
+
'-ExecutionPolicy', 'Bypass',
|
|
317
|
+
'-File', script,
|
|
318
|
+
'-ApiBaseUrl', OFFICIAL_API_ROOT,
|
|
319
|
+
'-WaitTimeoutSeconds', String(USAGE_VALIDATION_TIMEOUT_SECONDS),
|
|
320
|
+
], {
|
|
321
|
+
timeoutMs: (USAGE_VALIDATION_TIMEOUT_SECONDS + 30) * 1000,
|
|
322
|
+
maxBuffer: MAX_BUFFER,
|
|
323
|
+
env: windowsPowerShellEnvironment(powershell, dependencies.environment ?? process.env),
|
|
324
|
+
}, dependencies);
|
|
325
|
+
const parsed = parseSingleJson(executed?.stdout ?? executed);
|
|
326
|
+
if (!parsed || !validateSelfTestOutput(parsed)) return result('issue', 'usage-validation-output-invalid');
|
|
327
|
+
const cleanupWarning = parsed.cleanupWarning ? 'cleanup-warning' : null;
|
|
328
|
+
if (parsed.status === 'passed' && parsed.ok === true) {
|
|
329
|
+
return result('passed', 'usage-validation-passed', cleanupWarning);
|
|
330
|
+
}
|
|
331
|
+
if (parsed.status === 'issue' && parsed.ok === false) {
|
|
332
|
+
return result('issue', 'usage-validation-issue', cleanupWarning);
|
|
333
|
+
}
|
|
334
|
+
return result('issue', 'usage-validation-output-invalid', cleanupWarning);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const timedOut = error?.code === 'ETIMEDOUT' || error?.killed === true || error?.signal === 'SIGTERM';
|
|
337
|
+
return result('issue', timedOut ? 'usage-validation-timeout' : 'usage-validation-issue');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export const __test = Object.freeze({
|
|
342
|
+
parseSingleJson,
|
|
343
|
+
parseEnrollmentEvent,
|
|
344
|
+
validateSelfTestOutput,
|
|
345
|
+
isExactOfficialApiRoot,
|
|
346
|
+
invokeValidationProcess,
|
|
347
|
+
validatePowerShell,
|
|
348
|
+
windowsPowerShellEnvironment,
|
|
349
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rover-studio/answer-me",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Deterministic AnswerMe Toolkit installer for Codex on Windows x64",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"answerme-toolkit": "bin/answerme-toolkit.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20",
|
|
11
|
+
"npm": ">=7"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED"
|
|
17
|
+
}
|