@tiangong-ai/cli 0.0.28 → 0.0.29
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/AGENTS.md +1 -1
- package/README.md +31 -13
- package/dist/research/setup-command.js +44 -16
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/credentials.d.ts +15 -0
- package/dist/research/workspace/credentials.js +29 -8
- package/dist/research/workspace/credentials.js.map +1 -1
- package/dist/research/workspace/setup-wizard.d.ts +6 -0
- package/dist/research/workspace/setup-wizard.js +364 -126
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +13 -0
- package/dist/research/workspace/setup.js +108 -61
- package/dist/research/workspace/setup.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { closeSync, openSync } from "node:fs";
|
|
1
2
|
import { lstat, mkdir } from "node:fs/promises";
|
|
2
3
|
import { isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { Writable } from "node:stream";
|
|
6
|
+
import { ReadStream as TtyReadStream } from "node:tty";
|
|
4
7
|
import { CliError } from "../../errors.js";
|
|
5
8
|
import { stringifyJson, write } from "../../io.js";
|
|
6
9
|
import { parseStrictArgs, strictBoolean, strictString } from "../../strict-args.js";
|
|
@@ -57,10 +60,96 @@ const DEFAULT_CREDENTIAL_ENVIRONMENT = {
|
|
|
57
60
|
"tiangong.unstructure.auth-token": "UNSTRUCTURED_AUTH_TOKEN",
|
|
58
61
|
"semantic-scholar.api-key": "SEMANTIC_SCHOLAR_API_KEY",
|
|
59
62
|
};
|
|
63
|
+
const MAX_CREDENTIAL_STDIN_BYTES = 64 * 1024;
|
|
64
|
+
function credentialStdinIds(value) {
|
|
65
|
+
if (!value)
|
|
66
|
+
return [];
|
|
67
|
+
const ids = value
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((id) => id.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
if (!ids.length || new Set(ids).size !== ids.length) {
|
|
72
|
+
throw wizardStdinError("--credential-stdin requires unique logical credential IDs.");
|
|
73
|
+
}
|
|
74
|
+
const known = new Set(RESEARCH_SETUP_CREDENTIALS.map((credential) => credential.id));
|
|
75
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
76
|
+
if (unknown.length) {
|
|
77
|
+
throw wizardStdinError(`Unknown logical credential IDs: ${unknown.join(", ")}.`);
|
|
78
|
+
}
|
|
79
|
+
return ids;
|
|
80
|
+
}
|
|
81
|
+
export async function readResearchSetupCredentialStdin(input, credentialIds) {
|
|
82
|
+
if (!input || input.isTTY) {
|
|
83
|
+
throw wizardStdinError("Credential stdin requires a non-interactive input pipe.");
|
|
84
|
+
}
|
|
85
|
+
let totalBytes = 0;
|
|
86
|
+
const chunks = [];
|
|
87
|
+
for await (const chunk of input) {
|
|
88
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
89
|
+
totalBytes += buffer.byteLength;
|
|
90
|
+
if (totalBytes > MAX_CREDENTIAL_STDIN_BYTES) {
|
|
91
|
+
throw wizardStdinError("Credential stdin exceeds the supported bounded input size.");
|
|
92
|
+
}
|
|
93
|
+
chunks.push(buffer);
|
|
94
|
+
}
|
|
95
|
+
const lines = Buffer.concat(chunks).toString("utf8").split(/\r?\n/);
|
|
96
|
+
while (lines.at(-1) === "")
|
|
97
|
+
lines.pop();
|
|
98
|
+
if (lines.length !== credentialIds.length) {
|
|
99
|
+
throw wizardStdinError(`Credential stdin must contain exactly one line for each of ${credentialIds.length} listed logical credential IDs.`);
|
|
100
|
+
}
|
|
101
|
+
const result = {};
|
|
102
|
+
for (const [index, credentialId] of credentialIds.entries()) {
|
|
103
|
+
const value = lines[index]?.trim() ?? "";
|
|
104
|
+
const definition = RESEARCH_SETUP_CREDENTIALS.find((credential) => credential.id === credentialId);
|
|
105
|
+
if (Buffer.byteLength(value, "utf8") < definition.minimumUtf8Bytes) {
|
|
106
|
+
clearCredentialRecord(result);
|
|
107
|
+
throw wizardStdinError(`Credential stdin value for ${credentialId} is absent or does not meet the provider minimum.`);
|
|
108
|
+
}
|
|
109
|
+
result[credentialId] = value;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function openResearchSetupControllingTerminal() {
|
|
114
|
+
let descriptor;
|
|
115
|
+
try {
|
|
116
|
+
descriptor = openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw wizardStdinError("A controlling terminal is required when credential values are piped to the Wizard.");
|
|
120
|
+
}
|
|
121
|
+
const input = new TtyReadStream(descriptor);
|
|
122
|
+
return {
|
|
123
|
+
input,
|
|
124
|
+
close: () => {
|
|
125
|
+
try {
|
|
126
|
+
if (input.isRaw)
|
|
127
|
+
input.setRawMode(false);
|
|
128
|
+
input.destroy();
|
|
129
|
+
closeSync(descriptor);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Best-effort terminal cleanup after readline has already closed.
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function clearCredentialRecord(record) {
|
|
138
|
+
const mutable = record;
|
|
139
|
+
for (const id of Object.keys(mutable)) {
|
|
140
|
+
mutable[id] = "";
|
|
141
|
+
delete mutable[id];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
60
144
|
export async function runResearchSetupWizard(argv, io) {
|
|
61
|
-
const args = parseStrictArgs(argv, {
|
|
145
|
+
const args = parseStrictArgs(argv, {
|
|
146
|
+
help: "boolean",
|
|
147
|
+
json: "boolean",
|
|
148
|
+
workspace: "string",
|
|
149
|
+
"credential-stdin": "string",
|
|
150
|
+
}, "research setup wizard");
|
|
62
151
|
if (strictBoolean(args, "help")) {
|
|
63
|
-
write(io.stdout, "Usage: tiangong-ai research setup [--workspace <absolute-path>] [--json]\n");
|
|
152
|
+
write(io.stdout, "Usage: tiangong-ai research setup [--workspace <absolute-path>] [--credential-stdin <logical-id[,logical-id...]>] [--json]\n");
|
|
64
153
|
return 0;
|
|
65
154
|
}
|
|
66
155
|
if (args.positionals.length) {
|
|
@@ -69,7 +158,8 @@ export async function runResearchSetupWizard(argv, io) {
|
|
|
69
158
|
exitCode: 2,
|
|
70
159
|
});
|
|
71
160
|
}
|
|
72
|
-
|
|
161
|
+
const stdinCredentialIds = credentialStdinIds(strictString(args, "credential-stdin"));
|
|
162
|
+
if (!stdinCredentialIds.length && !io.stdin?.isTTY) {
|
|
73
163
|
throw new CliError("Interactive research setup requires a TTY.", {
|
|
74
164
|
code: "RESEARCH_SETUP_TTY_REQUIRED",
|
|
75
165
|
exitCode: 2,
|
|
@@ -81,8 +171,18 @@ export async function runResearchSetupWizard(argv, io) {
|
|
|
81
171
|
},
|
|
82
172
|
});
|
|
83
173
|
}
|
|
174
|
+
if (stdinCredentialIds.length && io.stdin?.isTTY) {
|
|
175
|
+
throw wizardStdinError("--credential-stdin requires a non-interactive pipe; use secure input for a terminal value.");
|
|
176
|
+
}
|
|
177
|
+
const stdinCredentials = stdinCredentialIds.length
|
|
178
|
+
? await readResearchSetupCredentialStdin(io.stdin, stdinCredentialIds)
|
|
179
|
+
: {};
|
|
180
|
+
const controllingTerminal = stdinCredentialIds.length
|
|
181
|
+
? openResearchSetupControllingTerminal()
|
|
182
|
+
: null;
|
|
183
|
+
const interactiveInput = controllingTerminal?.input ?? io.stdin;
|
|
84
184
|
const json = strictBoolean(args, "json");
|
|
85
|
-
const prompt = new TextResearchSetupWizardPrompt(
|
|
185
|
+
const prompt = new TextResearchSetupWizardPrompt(interactiveInput, io.stderr, createResearchSetupWizardTheme(shouldUseResearchSetupWizardColor({
|
|
86
186
|
outputIsTTY: Boolean(io.stderr.isTTY),
|
|
87
187
|
json,
|
|
88
188
|
environment: io.env,
|
|
@@ -93,10 +193,36 @@ export async function runResearchSetupWizard(argv, io) {
|
|
|
93
193
|
...(workspace === undefined ? {} : { workspace }),
|
|
94
194
|
environment: io.env,
|
|
95
195
|
prompt,
|
|
196
|
+
stdinCredentials,
|
|
96
197
|
});
|
|
97
198
|
write(io.stdout, stringifyJson(result.value, json));
|
|
98
199
|
return result.exitCode;
|
|
99
200
|
}
|
|
201
|
+
finally {
|
|
202
|
+
prompt.close();
|
|
203
|
+
controllingTerminal?.close();
|
|
204
|
+
clearCredentialRecord(stdinCredentials);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export async function promptResearchSetupCredentialValue(io, credentialId, json) {
|
|
208
|
+
if (!io.stdin?.isTTY) {
|
|
209
|
+
throw new CliError("Secure credential input requires a TTY.", {
|
|
210
|
+
code: "RESEARCH_SETUP_TTY_REQUIRED",
|
|
211
|
+
exitCode: 2,
|
|
212
|
+
details: {
|
|
213
|
+
step: "credentials",
|
|
214
|
+
minimumAction: "Run with --prompt in a terminal, or pipe the value with --from-stdin.",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const prompt = new TextResearchSetupWizardPrompt(io.stdin, io.stderr, createResearchSetupWizardTheme(shouldUseResearchSetupWizardColor({
|
|
219
|
+
outputIsTTY: Boolean(io.stderr.isTTY),
|
|
220
|
+
json,
|
|
221
|
+
environment: io.env,
|
|
222
|
+
})));
|
|
223
|
+
try {
|
|
224
|
+
return await prompt.secret(`Secure value for ${credentialId} (input hidden)`);
|
|
225
|
+
}
|
|
100
226
|
finally {
|
|
101
227
|
prompt.close();
|
|
102
228
|
}
|
|
@@ -106,7 +232,7 @@ export async function executeResearchSetupWizard(input) {
|
|
|
106
232
|
prompt.note([
|
|
107
233
|
"Tiangong Auto Research setup",
|
|
108
234
|
"No Skill is bundled or installed until you review the exact plan and confirm it.",
|
|
109
|
-
"Credentials
|
|
235
|
+
"Credentials may be entered securely, read from an owner environment variable, or preloaded from stdin. Values are never displayed or written to the plan.",
|
|
110
236
|
].join("\n"), "brand");
|
|
111
237
|
prompt.note("1. Workspace", "section");
|
|
112
238
|
const defaultWorkspace = resolve(input.workspace ?? process.cwd());
|
|
@@ -224,120 +350,127 @@ export async function executeResearchSetupWizard(input) {
|
|
|
224
350
|
}
|
|
225
351
|
prompt.note("4. Configuration and licenses", "section");
|
|
226
352
|
const settings = await collectSettings(selected.map((skill) => skill.id), prompt);
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
353
|
+
const credentials = await collectCredentialSources(selected.map((skill) => skill.id), input.environment, input.stdinCredentials ?? {}, prompt);
|
|
354
|
+
try {
|
|
355
|
+
const acceptedLicenseIds = await collectLicenseAcceptances(selected.map((skill) => skill.id), prompt);
|
|
356
|
+
const agentRoutes = await collectAgentRoutes(prompt);
|
|
357
|
+
prompt.note("5. Verification options", "section");
|
|
358
|
+
const liveChecks = await prompt.confirm("Run live provider checks after installation? This uses network/quota but does not run model agents.", false);
|
|
359
|
+
const allowSyntheticUnstructureUpload = liveChecks && selected.some((skill) => skill.id === "tiangong.document-granular-decompose")
|
|
360
|
+
? await prompt.confirm("Authorize upload of a generated one-page PDF to the configured Unstructure service?", false)
|
|
361
|
+
: false;
|
|
362
|
+
const agentSmoke = await prompt.confirm("Run producer/reviewer agent smoke checks after installation? This may consume paid model quota.", false);
|
|
363
|
+
const confirmAgentSmokeCost = agentSmoke
|
|
364
|
+
? await prompt.confirm("I explicitly authorize the agent smoke-check cost", false)
|
|
365
|
+
: false;
|
|
366
|
+
if (agentSmoke && !confirmAgentSmokeCost)
|
|
367
|
+
throw wizardCancelled("agent-smoke-confirmation");
|
|
368
|
+
const missingRequiredCredentialIds = requiredCredentialIds(selected.map((skill) => skill.id).filter(Boolean)).filter((id) => !credentials.environmentBindings[id]);
|
|
369
|
+
const preview = {
|
|
370
|
+
workspace,
|
|
371
|
+
createWorkspaceDirectory,
|
|
372
|
+
mode,
|
|
373
|
+
evidenceProfile,
|
|
374
|
+
selectedSkillIds: selected.map((skill) => skill.id),
|
|
375
|
+
install: {
|
|
376
|
+
scope,
|
|
377
|
+
agents,
|
|
378
|
+
targets: agents.map((agent) => ({
|
|
379
|
+
agent,
|
|
380
|
+
root: setupTargetRoot({ workspace, scope, agent, environment: input.environment }),
|
|
381
|
+
})),
|
|
382
|
+
},
|
|
383
|
+
installer: RESEARCH_SETUP_INSTALLER,
|
|
384
|
+
sourcePins: [...new Set(selected.map((skill) => skill.sourceId))].map((sourceId) => {
|
|
385
|
+
const source = setupSource(sourceId);
|
|
386
|
+
return {
|
|
387
|
+
id: source.id,
|
|
388
|
+
locator: source.locator,
|
|
389
|
+
immutableRef: source.immutableRef,
|
|
390
|
+
};
|
|
391
|
+
}),
|
|
392
|
+
skillPins: selected.map((skill) => ({
|
|
393
|
+
id: skill.id,
|
|
394
|
+
role: skill.role,
|
|
395
|
+
expectedTreeSha256: skill.expectedTreeSha256,
|
|
396
|
+
licenseId: skill.license.id,
|
|
397
|
+
})),
|
|
398
|
+
acceptedLicenseIds,
|
|
399
|
+
credentialSources: credentials.preview,
|
|
400
|
+
missingRequiredCredentialIds,
|
|
401
|
+
checks: { liveChecks, allowSyntheticUnstructureUpload, agentSmoke },
|
|
402
|
+
networkDownloads: selected.length > 0,
|
|
403
|
+
};
|
|
404
|
+
prompt.note("6. Review and apply", "section");
|
|
405
|
+
prompt.note(`Reviewed setup preview:\n${JSON.stringify(preview, null, 2)}`, "summary");
|
|
406
|
+
const confirmNetworkDownloads = selected.length === 0 ||
|
|
407
|
+
(await prompt.confirm("Authorize downloads of only the displayed pinned npm package and git commits?", false));
|
|
408
|
+
if (!confirmNetworkDownloads)
|
|
409
|
+
throw wizardCancelled("network-confirmation");
|
|
410
|
+
if (!(await prompt.confirm("Create this immutable setup plan?", false))) {
|
|
411
|
+
throw wizardCancelled("plan-confirmation");
|
|
412
|
+
}
|
|
413
|
+
if (createWorkspaceDirectory)
|
|
414
|
+
await mkdir(workspace);
|
|
415
|
+
const replacePlan = await pathExists(workspacePaths(workspace).setupPlan);
|
|
416
|
+
const plan = await createResearchSetupPlan({
|
|
417
|
+
workspace,
|
|
418
|
+
mode,
|
|
419
|
+
evidenceProfile,
|
|
420
|
+
skillIds: explicitSkillIds,
|
|
255
421
|
scope,
|
|
256
422
|
agents,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
423
|
+
acceptedLicenseIds,
|
|
424
|
+
credentialEnvironment: credentials.environmentBindings,
|
|
425
|
+
settings,
|
|
426
|
+
agentRoutes,
|
|
427
|
+
liveChecks,
|
|
428
|
+
allowSyntheticUnstructureUpload,
|
|
429
|
+
agentSmoke,
|
|
430
|
+
confirmNetworkDownloads,
|
|
431
|
+
confirmGlobalMutation,
|
|
432
|
+
confirmAgentSmokeCost,
|
|
433
|
+
replacePlan,
|
|
434
|
+
environment: input.environment,
|
|
435
|
+
});
|
|
436
|
+
prompt.note(`Plan created: ${workspacePaths(workspace).setupPlan}\nSHA-256: ${plan.planSha256}`, "success");
|
|
437
|
+
if (missingRequiredCredentialIds.length) {
|
|
438
|
+
prompt.note(`Apply is blocked until these required logical credentials are configured: ${missingRequiredCredentialIds.join(", ")}`, "warning");
|
|
439
|
+
}
|
|
440
|
+
else if (credentials.hasTransientValues) {
|
|
441
|
+
prompt.note("Securely entered or stdin values exist only for this apply. Applying now stores them in the owner-only credential store before downloads; choosing no discards them.", "info");
|
|
442
|
+
}
|
|
443
|
+
const applyNow = await prompt.confirm(missingRequiredCredentialIds.length
|
|
444
|
+
? "Apply now anyway? Preflight will stop before downloads because required credentials are absent."
|
|
445
|
+
: "Apply the reviewed plan now?", missingRequiredCredentialIds.length === 0);
|
|
446
|
+
if (!applyNow) {
|
|
265
447
|
return {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
448
|
+
exitCode: 0,
|
|
449
|
+
value: {
|
|
450
|
+
schemaVersion: 1,
|
|
451
|
+
status: "planned",
|
|
452
|
+
plan,
|
|
453
|
+
next: credentials.hasTransientValues
|
|
454
|
+
? {
|
|
455
|
+
minimumAction: "Secure and stdin values were discarded. Configure those logical credentials again before apply.",
|
|
456
|
+
credentialCommands: credentials.preview
|
|
457
|
+
.filter((credential) => credential.inputMethod === "secure-input" ||
|
|
458
|
+
credential.inputMethod === "stdin")
|
|
459
|
+
.map((credential) => `tiangong-ai research setup credential set --id ${credential.id} --prompt --workspace ${workspace} --json`),
|
|
460
|
+
applyCommand: `tiangong-ai research setup apply --plan ${workspacePaths(workspace).setupPlan} --json`,
|
|
461
|
+
}
|
|
462
|
+
: `tiangong-ai research setup apply --plan ${workspacePaths(workspace).setupPlan} --json`,
|
|
463
|
+
},
|
|
269
464
|
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
configuredCredentialEnvironmentNames: Object.entries(credentialEnvironment).map(([id, environmentName]) => ({
|
|
279
|
-
id,
|
|
280
|
-
environmentName,
|
|
281
|
-
present: Boolean(input.environment[environmentName]),
|
|
282
|
-
})),
|
|
283
|
-
missingRequiredCredentialIds,
|
|
284
|
-
checks: { liveChecks, allowSyntheticUnstructureUpload, agentSmoke },
|
|
285
|
-
networkDownloads: selected.length > 0,
|
|
286
|
-
};
|
|
287
|
-
prompt.note("6. Review and apply", "section");
|
|
288
|
-
prompt.note(`Reviewed setup preview:\n${JSON.stringify(preview, null, 2)}`, "summary");
|
|
289
|
-
const confirmNetworkDownloads = selected.length === 0 ||
|
|
290
|
-
(await prompt.confirm("Authorize downloads of only the displayed pinned npm package and git commits?", false));
|
|
291
|
-
if (!confirmNetworkDownloads)
|
|
292
|
-
throw wizardCancelled("network-confirmation");
|
|
293
|
-
if (!(await prompt.confirm("Create this immutable setup plan?", false))) {
|
|
294
|
-
throw wizardCancelled("plan-confirmation");
|
|
295
|
-
}
|
|
296
|
-
if (createWorkspaceDirectory)
|
|
297
|
-
await mkdir(workspace);
|
|
298
|
-
const replacePlan = await pathExists(workspacePaths(workspace).setupPlan);
|
|
299
|
-
const plan = await createResearchSetupPlan({
|
|
300
|
-
workspace,
|
|
301
|
-
mode,
|
|
302
|
-
evidenceProfile,
|
|
303
|
-
skillIds: explicitSkillIds,
|
|
304
|
-
scope,
|
|
305
|
-
agents,
|
|
306
|
-
acceptedLicenseIds,
|
|
307
|
-
credentialEnvironment,
|
|
308
|
-
settings,
|
|
309
|
-
agentRoutes,
|
|
310
|
-
liveChecks,
|
|
311
|
-
allowSyntheticUnstructureUpload,
|
|
312
|
-
agentSmoke,
|
|
313
|
-
confirmNetworkDownloads,
|
|
314
|
-
confirmGlobalMutation,
|
|
315
|
-
confirmAgentSmokeCost,
|
|
316
|
-
replacePlan,
|
|
317
|
-
environment: input.environment,
|
|
318
|
-
});
|
|
319
|
-
prompt.note(`Plan created: ${workspacePaths(workspace).setupPlan}\nSHA-256: ${plan.planSha256}`, "success");
|
|
320
|
-
if (missingRequiredCredentialIds.length) {
|
|
321
|
-
prompt.note(`Apply is blocked until these required owner environment variables are set: ${missingRequiredCredentialIds.join(", ")}`, "warning");
|
|
322
|
-
}
|
|
323
|
-
const applyNow = await prompt.confirm(missingRequiredCredentialIds.length
|
|
324
|
-
? "Apply now anyway? Preflight will stop before downloads because required credentials are absent."
|
|
325
|
-
: "Apply the reviewed plan now?", false);
|
|
326
|
-
if (!applyNow) {
|
|
327
|
-
return {
|
|
328
|
-
exitCode: 0,
|
|
329
|
-
value: {
|
|
330
|
-
schemaVersion: 1,
|
|
331
|
-
status: "planned",
|
|
332
|
-
plan,
|
|
333
|
-
next: `tiangong-ai research setup apply --plan ${workspacePaths(workspace).setupPlan} --json`,
|
|
334
|
-
},
|
|
335
|
-
};
|
|
465
|
+
}
|
|
466
|
+
const value = await applyResearchSetupPlan(workspacePaths(workspace).setupPlan, {
|
|
467
|
+
environment: credentials.applyEnvironment,
|
|
468
|
+
});
|
|
469
|
+
return { exitCode: value.state.status === "blocked" ? 3 : 0, value };
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
credentials.clear();
|
|
336
473
|
}
|
|
337
|
-
const value = await applyResearchSetupPlan(workspacePaths(workspace).setupPlan, {
|
|
338
|
-
environment: input.environment,
|
|
339
|
-
});
|
|
340
|
-
return { exitCode: value.state.status === "blocked" ? 3 : 0, value };
|
|
341
474
|
}
|
|
342
475
|
function postClosureAuthoringRank(skillId) {
|
|
343
476
|
const guidance = RESEARCH_SETUP_SELECTION_GUIDANCE.pptCreation;
|
|
@@ -357,24 +490,90 @@ async function collectSettings(selectedSkillIds, prompt) {
|
|
|
357
490
|
}
|
|
358
491
|
return settings;
|
|
359
492
|
}
|
|
360
|
-
async function collectCredentialSources(selectedSkillIds, environment, prompt) {
|
|
493
|
+
async function collectCredentialSources(selectedSkillIds, environment, stdinCredentials, prompt) {
|
|
361
494
|
const selected = new Set(selectedSkillIds);
|
|
362
|
-
const
|
|
363
|
-
|
|
495
|
+
const preloadedStdinCredentials = { ...stdinCredentials };
|
|
496
|
+
const definitions = RESEARCH_SETUP_CREDENTIALS.filter((item) => item.requiredBy.some((skillId) => selected.has(skillId)));
|
|
497
|
+
const selectedCredentialIds = new Set(definitions.map((credential) => credential.id));
|
|
498
|
+
const unexpectedStdinIds = Object.keys(preloadedStdinCredentials).filter((id) => !selectedCredentialIds.has(id));
|
|
499
|
+
if (unexpectedStdinIds.length) {
|
|
500
|
+
throw wizardStdinError(`Preloaded stdin credentials were not selected in this plan: ${unexpectedStdinIds.join(", ")}.`);
|
|
501
|
+
}
|
|
502
|
+
const environmentBindings = {};
|
|
503
|
+
const applyEnvironment = { ...environment };
|
|
504
|
+
const transientEnvironmentNames = [];
|
|
505
|
+
const preview = [];
|
|
506
|
+
for (const credential of definitions) {
|
|
364
507
|
const defaultEnvironmentName = DEFAULT_CREDENTIAL_ENVIRONMENT[credential.id];
|
|
365
508
|
const present = Buffer.byteLength(environment[defaultEnvironmentName] ?? "", "utf8") >=
|
|
366
509
|
credential.minimumUtf8Bytes;
|
|
367
|
-
prompt.note(`${credential.provider}\n credential: ${credential.id}\n obtain/configure: ${credential.obtainAt}\n ${defaultEnvironmentName}
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
510
|
+
prompt.note(`${credential.provider}\n credential: ${credential.id}\n obtain/configure: ${credential.obtainAt}\n default environment: ${defaultEnvironmentName} (${present ? "present" : "not present"})`, present ? "info" : "warning");
|
|
511
|
+
for (;;) {
|
|
512
|
+
const inputMethod = await prompt.select(`Credential source for ${credential.id}`, [
|
|
513
|
+
{ value: "secure-input", label: "Enter securely now (recommended)" },
|
|
514
|
+
{ value: "environment", label: "Read from an environment variable" },
|
|
515
|
+
{ value: "stdin", label: "Read from stdin / password manager" },
|
|
516
|
+
{ value: "skipped", label: "Skip for now" },
|
|
517
|
+
], credential.required ? "secure-input" : "skipped");
|
|
518
|
+
if (inputMethod === "skipped") {
|
|
519
|
+
preview.push({ id: credential.id, inputMethod, configured: false });
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
if (inputMethod === "environment") {
|
|
523
|
+
const environmentName = (await prompt.input(`Environment variable name for ${credential.id} (never the secret value)`, defaultEnvironmentName)).trim();
|
|
524
|
+
const value = environment[environmentName];
|
|
525
|
+
if (!environmentName ||
|
|
526
|
+
!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(environmentName) ||
|
|
527
|
+
Buffer.byteLength(value ?? "", "utf8") < credential.minimumUtf8Bytes) {
|
|
528
|
+
prompt.note("The named environment variable is absent or does not meet the provider minimum. Choose another source or skip explicitly.", "warning");
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
environmentBindings[credential.id] = environmentName;
|
|
532
|
+
preview.push({
|
|
533
|
+
id: credential.id,
|
|
534
|
+
inputMethod,
|
|
535
|
+
environmentName,
|
|
536
|
+
configured: true,
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
const value = inputMethod === "secure-input"
|
|
541
|
+
? await prompt.secret(`Secure value for ${credential.id} (input hidden)`)
|
|
542
|
+
: preloadedStdinCredentials[credential.id];
|
|
543
|
+
if (inputMethod === "stdin" && value === undefined) {
|
|
544
|
+
prompt.note(`No stdin value was preloaded for ${credential.id}. Restart with --credential-stdin ${credential.id} and pipe one value line, or choose secure input.`, "warning");
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (Buffer.byteLength(value ?? "", "utf8") < credential.minimumUtf8Bytes) {
|
|
548
|
+
prompt.note("The credential is absent or does not meet the selected provider minimum; no part of it was retained or displayed.", "warning");
|
|
371
549
|
continue;
|
|
550
|
+
}
|
|
551
|
+
const transientName = transientCredentialEnvironmentName(credential.id, inputMethod);
|
|
552
|
+
environmentBindings[credential.id] = transientName;
|
|
553
|
+
applyEnvironment[transientName] = value;
|
|
554
|
+
transientEnvironmentNames.push(transientName);
|
|
555
|
+
preview.push({ id: credential.id, inputMethod, configured: true });
|
|
556
|
+
break;
|
|
372
557
|
}
|
|
373
|
-
const environmentName = await prompt.input(`Environment variable name for ${credential.id} (never the secret value)`, defaultEnvironmentName);
|
|
374
|
-
if (environmentName.trim())
|
|
375
|
-
result[credential.id] = environmentName.trim();
|
|
376
558
|
}
|
|
377
|
-
return
|
|
559
|
+
return {
|
|
560
|
+
environmentBindings,
|
|
561
|
+
applyEnvironment,
|
|
562
|
+
preview,
|
|
563
|
+
hasTransientValues: transientEnvironmentNames.length > 0,
|
|
564
|
+
clear: () => {
|
|
565
|
+
for (const name of transientEnvironmentNames) {
|
|
566
|
+
applyEnvironment[name] = "";
|
|
567
|
+
delete applyEnvironment[name];
|
|
568
|
+
}
|
|
569
|
+
clearCredentialRecord(preloadedStdinCredentials);
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function transientCredentialEnvironmentName(credentialId, inputMethod) {
|
|
574
|
+
const logical = credentialId.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
575
|
+
const source = inputMethod === "secure-input" ? "SECURE_INPUT" : "STDIN";
|
|
576
|
+
return `TIANGONG_RESEARCH_SETUP_${source}_${logical}`.slice(0, 128);
|
|
378
577
|
}
|
|
379
578
|
async function collectLicenseAcceptances(selectedSkillIds, prompt) {
|
|
380
579
|
const selected = resolveSetupSkills(selectedSkillIds);
|
|
@@ -449,16 +648,33 @@ function requiredCredentialIds(selectedSkillIds) {
|
|
|
449
648
|
const selected = new Set(selectedSkillIds);
|
|
450
649
|
return RESEARCH_SETUP_CREDENTIALS.filter((credential) => credential.required && credential.requiredBy.some((skillId) => selected.has(skillId))).map((credential) => credential.id);
|
|
451
650
|
}
|
|
651
|
+
class ResearchSetupReadlineOutput extends Writable {
|
|
652
|
+
target;
|
|
653
|
+
isTTY = true;
|
|
654
|
+
columns = 80;
|
|
655
|
+
muted = false;
|
|
656
|
+
constructor(target) {
|
|
657
|
+
super();
|
|
658
|
+
this.target = target;
|
|
659
|
+
}
|
|
660
|
+
_write(chunk, _encoding, callback) {
|
|
661
|
+
if (!this.muted)
|
|
662
|
+
write(this.target, chunk.toString());
|
|
663
|
+
callback();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
452
666
|
class TextResearchSetupWizardPrompt {
|
|
453
667
|
#readline;
|
|
668
|
+
#readlineOutput;
|
|
454
669
|
#output;
|
|
455
670
|
#theme;
|
|
456
671
|
constructor(input, output, theme) {
|
|
457
672
|
this.#output = output;
|
|
458
673
|
this.#theme = theme;
|
|
674
|
+
this.#readlineOutput = new ResearchSetupReadlineOutput(output);
|
|
459
675
|
this.#readline = createInterface({
|
|
460
676
|
input,
|
|
461
|
-
output:
|
|
677
|
+
output: this.#readlineOutput,
|
|
462
678
|
terminal: true,
|
|
463
679
|
});
|
|
464
680
|
}
|
|
@@ -471,6 +687,18 @@ class TextResearchSetupWizardPrompt {
|
|
|
471
687
|
const answer = (await this.#readline.question(question)).trim();
|
|
472
688
|
return answer || defaultValue;
|
|
473
689
|
}
|
|
690
|
+
async secret(message) {
|
|
691
|
+
const question = `${this.#theme.accent("?")} ${this.#theme.heading(message)}: `;
|
|
692
|
+
write(this.#output, question);
|
|
693
|
+
this.#readlineOutput.muted = true;
|
|
694
|
+
try {
|
|
695
|
+
return (await this.#readline.question("")).trim();
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
this.#readlineOutput.muted = false;
|
|
699
|
+
write(this.#output, "\n");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
474
702
|
async confirm(message, defaultValue) {
|
|
475
703
|
const suffix = this.#theme.muted(defaultValue ? " [Y/n]" : " [y/N]");
|
|
476
704
|
for (;;) {
|
|
@@ -541,4 +769,14 @@ function wizardError(message, step) {
|
|
|
541
769
|
details: { step, minimumAction: "Correct the displayed value and restart the Wizard." },
|
|
542
770
|
});
|
|
543
771
|
}
|
|
772
|
+
function wizardStdinError(message) {
|
|
773
|
+
return new CliError(message, {
|
|
774
|
+
code: "RESEARCH_SETUP_CREDENTIAL_STDIN_INVALID",
|
|
775
|
+
exitCode: 2,
|
|
776
|
+
details: {
|
|
777
|
+
step: "credentials",
|
|
778
|
+
minimumAction: "Use secure Wizard input, a named owner environment variable, or pipe one bounded line per explicitly listed logical credential ID.",
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
}
|
|
544
782
|
//# sourceMappingURL=setup-wizard.js.map
|