@tiangong-ai/cli 0.0.27 → 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.
Files changed (41) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +114 -48
  3. package/dist/research/orchestration.js +38 -0
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/setup-command.js +45 -17
  6. package/dist/research/setup-command.js.map +1 -1
  7. package/dist/research/workspace/broker.d.ts +5 -0
  8. package/dist/research/workspace/broker.js +98 -9
  9. package/dist/research/workspace/broker.js.map +1 -1
  10. package/dist/research/workspace/capabilities.js +32 -11
  11. package/dist/research/workspace/capabilities.js.map +1 -1
  12. package/dist/research/workspace/credentials.d.ts +21 -1
  13. package/dist/research/workspace/credentials.js +45 -10
  14. package/dist/research/workspace/credentials.js.map +1 -1
  15. package/dist/research/workspace/executor.js +43 -3
  16. package/dist/research/workspace/executor.js.map +1 -1
  17. package/dist/research/workspace/external-skills.d.ts +31 -0
  18. package/dist/research/workspace/external-skills.js +161 -13
  19. package/dist/research/workspace/external-skills.js.map +1 -1
  20. package/dist/research/workspace/preflight.d.ts +34 -2
  21. package/dist/research/workspace/preflight.js +76 -21
  22. package/dist/research/workspace/preflight.js.map +1 -1
  23. package/dist/research/workspace/runtime.js +135 -50
  24. package/dist/research/workspace/runtime.js.map +1 -1
  25. package/dist/research/workspace/schemas.d.ts +4 -0
  26. package/dist/research/workspace/schemas.js +29 -3
  27. package/dist/research/workspace/schemas.js.map +1 -1
  28. package/dist/research/workspace/setup-catalog.d.ts +3 -2
  29. package/dist/research/workspace/setup-catalog.js +22 -1
  30. package/dist/research/workspace/setup-catalog.js.map +1 -1
  31. package/dist/research/workspace/setup-wizard.d.ts +6 -0
  32. package/dist/research/workspace/setup-wizard.js +379 -132
  33. package/dist/research/workspace/setup-wizard.js.map +1 -1
  34. package/dist/research/workspace/setup.d.ts +17 -0
  35. package/dist/research/workspace/setup.js +190 -95
  36. package/dist/research/workspace/setup.js.map +1 -1
  37. package/dist/research/workspace/types.d.ts +2 -0
  38. package/dist/research/workspace/workspace.d.ts +2 -1
  39. package/dist/research/workspace/workspace.js +62 -13
  40. package/dist/research/workspace/workspace.js.map +1 -1
  41. 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, { help: "boolean", json: "boolean", workspace: "string" }, "research setup wizard");
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
- if (!io.stdin?.isTTY) {
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(io.stdin, io.stderr, createResearchSetupWizardTheme(shouldUseResearchSetupWizardColor({
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 are read only from owner environment variables; their values are never displayed.",
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());
@@ -157,21 +283,24 @@ export async function executeResearchSetupWizard(input) {
157
283
  { value: "smoke-test", label: "Smoke test (low-cost workflow validation)" },
158
284
  ], "production-research");
159
285
  const evidenceChoices = [
286
+ { value: EXTERNAL_SKILL_PROFILE, label: "Brave web + news (recommended baseline)" },
160
287
  {
161
288
  value: EXTERNAL_SKILL_CONTEXT_PROFILE,
162
- label: "Brave web + news + bounded full-text context (recommended)",
289
+ label: "Brave web + news + bounded context (requires provider plan support)",
163
290
  },
164
- { value: EXTERNAL_SKILL_PROFILE, label: "Brave web + news" },
165
291
  {
166
292
  value: EXTERNAL_SKILL_MEDIA_PROFILE,
167
- label: "Brave context + image/video discovery",
293
+ label: "Brave context + image/video discovery (subscription-dependent)",
168
294
  },
169
295
  ];
170
296
  if (mode === "smoke-test") {
171
297
  evidenceChoices.push({ value: "none", label: "No public internet (smoke test only)" });
172
298
  }
173
- const evidenceProfile = await prompt.select("Independent public-internet evidence profile", evidenceChoices, EXTERNAL_SKILL_CONTEXT_PROFILE);
174
- const companionChoices = RESEARCH_SETUP_SKILLS.filter((skill) => skill.sourceId === "tiangong-ai-skills" && skill.role !== "post-closure-authoring").map((skill) => ({ value: skill.id, label: `${skill.skillName} — ${skill.purpose}` }));
299
+ const evidenceProfile = await prompt.select("Independent public-internet evidence profile", evidenceChoices, EXTERNAL_SKILL_PROFILE);
300
+ const includeOrchestrator = await prompt.confirm("Install the tiangong-auto-research orchestrator so ordinary research requests route into this workspace workflow?", true);
301
+ const companionChoices = RESEARCH_SETUP_SKILLS.filter((skill) => skill.sourceId === "tiangong-ai-skills" &&
302
+ skill.role !== "orchestrator" &&
303
+ skill.role !== "post-closure-authoring").map((skill) => ({ value: skill.id, label: `${skill.skillName} — ${skill.purpose}` }));
175
304
  const companionIds = await prompt.multiSelect("Optional Tiangong companion Skills (explicit selection)", companionChoices, companionChoices
176
305
  .map((choice) => RESEARCH_SETUP_SKILLS.find((skill) => skill.id === choice.value))
177
306
  .filter((skill) => skill.defaultSelected)
@@ -184,7 +313,13 @@ export async function executeResearchSetupWizard(input) {
184
313
  .map((skill) => ({ value: skill.id, label: `${skill.skillName} — ${skill.purpose}` }));
185
314
  authoringIds = await prompt.multiSelect("Post-closure authoring Skills", choices, []);
186
315
  }
187
- const explicitSkillIds = [...new Set([...companionIds, ...authoringIds])];
316
+ const explicitSkillIds = [
317
+ ...new Set([
318
+ ...(includeOrchestrator ? ["tiangong.auto-research"] : []),
319
+ ...companionIds,
320
+ ...authoringIds,
321
+ ]),
322
+ ];
188
323
  const profileSkillIds = evidenceProfileSkillIds(evidenceProfile);
189
324
  const selected = resolveSetupSkills([...profileSkillIds, ...explicitSkillIds]);
190
325
  prompt.note("3. Installation targets", "section");
@@ -215,120 +350,127 @@ export async function executeResearchSetupWizard(input) {
215
350
  }
216
351
  prompt.note("4. Configuration and licenses", "section");
217
352
  const settings = await collectSettings(selected.map((skill) => skill.id), prompt);
218
- const credentialEnvironment = await collectCredentialSources(selected.map((skill) => skill.id), input.environment, prompt);
219
- const acceptedLicenseIds = await collectLicenseAcceptances(selected.map((skill) => skill.id), prompt);
220
- const agentRoutes = await collectAgentRoutes(prompt);
221
- prompt.note("5. Verification options", "section");
222
- const liveChecks = await prompt.confirm("Run live provider checks after installation? This uses network/quota but does not run model agents.", false);
223
- const allowSyntheticUnstructureUpload = liveChecks && selected.some((skill) => skill.id === "tiangong.document-granular-decompose")
224
- ? await prompt.confirm("Authorize upload of a generated one-page PDF to the configured Unstructure service?", false)
225
- : false;
226
- const agentSmoke = await prompt.confirm("Run producer/reviewer agent smoke checks after installation? This may consume paid model quota.", false);
227
- const confirmAgentSmokeCost = agentSmoke
228
- ? await prompt.confirm("I explicitly authorize the agent smoke-check cost", false)
229
- : false;
230
- if (agentSmoke && !confirmAgentSmokeCost)
231
- throw wizardCancelled("agent-smoke-confirmation");
232
- const missingRequiredCredentialIds = requiredCredentialIds(selected.map((skill) => skill.id).filter(Boolean)).filter((id) => {
233
- const environmentName = credentialEnvironment[id];
234
- const definition = RESEARCH_SETUP_CREDENTIALS.find((item) => item.id === id);
235
- return (!environmentName ||
236
- Buffer.byteLength(input.environment[environmentName] ?? "", "utf8") <
237
- definition.minimumUtf8Bytes);
238
- });
239
- const preview = {
240
- workspace,
241
- createWorkspaceDirectory,
242
- mode,
243
- evidenceProfile,
244
- selectedSkillIds: selected.map((skill) => skill.id),
245
- install: {
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,
246
421
  scope,
247
422
  agents,
248
- targets: agents.map((agent) => ({
249
- agent,
250
- root: setupTargetRoot({ workspace, scope, agent, environment: input.environment }),
251
- })),
252
- },
253
- installer: RESEARCH_SETUP_INSTALLER,
254
- sourcePins: [...new Set(selected.map((skill) => skill.sourceId))].map((sourceId) => {
255
- const source = setupSource(sourceId);
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) {
256
447
  return {
257
- id: source.id,
258
- locator: source.locator,
259
- immutableRef: source.immutableRef,
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
+ },
260
464
  };
261
- }),
262
- skillPins: selected.map((skill) => ({
263
- id: skill.id,
264
- role: skill.role,
265
- expectedTreeSha256: skill.expectedTreeSha256,
266
- licenseId: skill.license.id,
267
- })),
268
- acceptedLicenseIds,
269
- configuredCredentialEnvironmentNames: Object.entries(credentialEnvironment).map(([id, environmentName]) => ({
270
- id,
271
- environmentName,
272
- present: Boolean(input.environment[environmentName]),
273
- })),
274
- missingRequiredCredentialIds,
275
- checks: { liveChecks, allowSyntheticUnstructureUpload, agentSmoke },
276
- networkDownloads: selected.length > 0,
277
- };
278
- prompt.note("6. Review and apply", "section");
279
- prompt.note(`Reviewed setup preview:\n${JSON.stringify(preview, null, 2)}`, "summary");
280
- const confirmNetworkDownloads = selected.length === 0 ||
281
- (await prompt.confirm("Authorize downloads of only the displayed pinned npm package and git commits?", false));
282
- if (!confirmNetworkDownloads)
283
- throw wizardCancelled("network-confirmation");
284
- if (!(await prompt.confirm("Create this immutable setup plan?", false))) {
285
- throw wizardCancelled("plan-confirmation");
286
- }
287
- if (createWorkspaceDirectory)
288
- await mkdir(workspace);
289
- const replacePlan = await pathExists(workspacePaths(workspace).setupPlan);
290
- const plan = await createResearchSetupPlan({
291
- workspace,
292
- mode,
293
- evidenceProfile,
294
- skillIds: explicitSkillIds,
295
- scope,
296
- agents,
297
- acceptedLicenseIds,
298
- credentialEnvironment,
299
- settings,
300
- agentRoutes,
301
- liveChecks,
302
- allowSyntheticUnstructureUpload,
303
- agentSmoke,
304
- confirmNetworkDownloads,
305
- confirmGlobalMutation,
306
- confirmAgentSmokeCost,
307
- replacePlan,
308
- environment: input.environment,
309
- });
310
- prompt.note(`Plan created: ${workspacePaths(workspace).setupPlan}\nSHA-256: ${plan.planSha256}`, "success");
311
- if (missingRequiredCredentialIds.length) {
312
- prompt.note(`Apply is blocked until these required owner environment variables are set: ${missingRequiredCredentialIds.join(", ")}`, "warning");
313
- }
314
- const applyNow = await prompt.confirm(missingRequiredCredentialIds.length
315
- ? "Apply now anyway? Preflight will stop before downloads because required credentials are absent."
316
- : "Apply the reviewed plan now?", false);
317
- if (!applyNow) {
318
- return {
319
- exitCode: 0,
320
- value: {
321
- schemaVersion: 1,
322
- status: "planned",
323
- plan,
324
- next: `tiangong-ai research setup apply --plan ${workspacePaths(workspace).setupPlan} --json`,
325
- },
326
- };
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();
327
473
  }
328
- const value = await applyResearchSetupPlan(workspacePaths(workspace).setupPlan, {
329
- environment: input.environment,
330
- });
331
- return { exitCode: value.state.status === "blocked" ? 3 : 0, value };
332
474
  }
333
475
  function postClosureAuthoringRank(skillId) {
334
476
  const guidance = RESEARCH_SETUP_SELECTION_GUIDANCE.pptCreation;
@@ -348,24 +490,90 @@ async function collectSettings(selectedSkillIds, prompt) {
348
490
  }
349
491
  return settings;
350
492
  }
351
- async function collectCredentialSources(selectedSkillIds, environment, prompt) {
493
+ async function collectCredentialSources(selectedSkillIds, environment, stdinCredentials, prompt) {
352
494
  const selected = new Set(selectedSkillIds);
353
- const result = {};
354
- for (const credential of RESEARCH_SETUP_CREDENTIALS.filter((item) => item.requiredBy.some((skillId) => selected.has(skillId)))) {
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) {
355
507
  const defaultEnvironmentName = DEFAULT_CREDENTIAL_ENVIRONMENT[credential.id];
356
508
  const present = Buffer.byteLength(environment[defaultEnvironmentName] ?? "", "utf8") >=
357
509
  credential.minimumUtf8Bytes;
358
- prompt.note(`${credential.provider}\n credential: ${credential.id}\n obtain/configure: ${credential.obtainAt}\n ${defaultEnvironmentName}: ${present ? "present" : "not present"}`, present ? "info" : "warning");
359
- if (!credential.required && !present) {
360
- const configure = await prompt.confirm(`Configure optional ${credential.id} from an environment variable?`, false);
361
- if (!configure)
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");
362
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;
363
557
  }
364
- const environmentName = await prompt.input(`Environment variable name for ${credential.id} (never the secret value)`, defaultEnvironmentName);
365
- if (environmentName.trim())
366
- result[credential.id] = environmentName.trim();
367
558
  }
368
- return result;
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);
369
577
  }
370
578
  async function collectLicenseAcceptances(selectedSkillIds, prompt) {
371
579
  const selected = resolveSetupSkills(selectedSkillIds);
@@ -440,16 +648,33 @@ function requiredCredentialIds(selectedSkillIds) {
440
648
  const selected = new Set(selectedSkillIds);
441
649
  return RESEARCH_SETUP_CREDENTIALS.filter((credential) => credential.required && credential.requiredBy.some((skillId) => selected.has(skillId))).map((credential) => credential.id);
442
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
+ }
443
666
  class TextResearchSetupWizardPrompt {
444
667
  #readline;
668
+ #readlineOutput;
445
669
  #output;
446
670
  #theme;
447
671
  constructor(input, output, theme) {
448
672
  this.#output = output;
449
673
  this.#theme = theme;
674
+ this.#readlineOutput = new ResearchSetupReadlineOutput(output);
450
675
  this.#readline = createInterface({
451
676
  input,
452
- output: output,
677
+ output: this.#readlineOutput,
453
678
  terminal: true,
454
679
  });
455
680
  }
@@ -462,6 +687,18 @@ class TextResearchSetupWizardPrompt {
462
687
  const answer = (await this.#readline.question(question)).trim();
463
688
  return answer || defaultValue;
464
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
+ }
465
702
  async confirm(message, defaultValue) {
466
703
  const suffix = this.#theme.muted(defaultValue ? " [Y/n]" : " [y/N]");
467
704
  for (;;) {
@@ -532,4 +769,14 @@ function wizardError(message, step) {
532
769
  details: { step, minimumAction: "Correct the displayed value and restart the Wizard." },
533
770
  });
534
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
+ }
535
782
  //# sourceMappingURL=setup-wizard.js.map