@alessandroraffa/tangyr 0.11.1 → 0.12.0

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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +530 -58
  3. package/package.json +9 -3
package/README.md CHANGED
@@ -4,6 +4,10 @@ CLI for the Tangyr discipline — install and manage operating kits for AI codin
4
4
 
5
5
  An operating kit is a self-contained package of agents, skills, commands, rules, and templates that implements the Tangyr discipline for a specific AI coding context. The CLI places kit artifacts into the environments consumed by supported tools.
6
6
 
7
+ ## Requirements
8
+
9
+ Node.js `>=22`. Running `tangyr` on an older Node.js version prints a clear error to stderr and exits without attempting to run any command.
10
+
7
11
  ## Install / run
8
12
 
9
13
  ```sh
@@ -49,6 +53,16 @@ tangyr <command>
49
53
  | `tangyr config` | Display or edit Tangyr configuration |
50
54
  | `tangyr validate` | Validate the kit against kitFormat 1 conformity rules |
51
55
 
56
+ ### Authentication
57
+
58
+ | Command | Description |
59
+ | -------------------- | --------------------------------------------------------------- |
60
+ | `tangyr auth login` | Store a client ID/secret credential pair, validating by default |
61
+ | `tangyr auth status` | Show current credential validation state |
62
+ | `tangyr auth logout` | Remove the stored credential |
63
+
64
+ Resolved environment-first (`TANGYR_ACCESS_CLIENT_ID`/`TANGYR_ACCESS_CLIENT_SECRET`), then from the stored file, then interactively. A full quickstart (configuring a remote kit origin and installing against it) lands once remote kit resolution ships.
65
+
52
66
  ## Global flags
53
67
 
54
68
  | Flag | Description |
package/dist/index.js CHANGED
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x2) => typeof require !== "undefined" ? requir
14
14
  throw Error('Dynamic require of "' + x2 + '" is not supported');
15
15
  });
16
16
  var __commonJS = (cb, mod) => function __require2() {
17
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
18
22
  };
19
23
  var __copyProps = (to, from, except, desc) => {
20
24
  if (from && typeof from === "object" || typeof from === "function") {
@@ -14761,6 +14765,60 @@ var dist_default6 = createPrompt((config, done) => {
14761
14765
  ];
14762
14766
  });
14763
14767
 
14768
+ // node_modules/@inquirer/password/dist/index.js
14769
+ var passwordTheme = {
14770
+ style: {
14771
+ maskedText: "[input is masked]"
14772
+ }
14773
+ };
14774
+ var dist_default7 = createPrompt((config, done) => {
14775
+ const { validate = () => true } = config;
14776
+ const theme = makeTheme(passwordTheme, config.theme);
14777
+ const [status, setStatus] = useState("idle");
14778
+ const [errorMsg, setError] = useState();
14779
+ const [value, setValue] = useState("");
14780
+ const prefix = usePrefix({ status, theme });
14781
+ useKeypress(async (key, rl) => {
14782
+ if (status !== "idle") {
14783
+ return;
14784
+ }
14785
+ if (isEnterKey(key)) {
14786
+ const answer = value;
14787
+ setStatus("loading");
14788
+ const isValid = await validate(answer);
14789
+ if (isValid === true) {
14790
+ setValue(answer);
14791
+ setStatus("done");
14792
+ done(answer);
14793
+ } else {
14794
+ rl.write(value);
14795
+ setError(isValid || "You must provide a valid value");
14796
+ setStatus("idle");
14797
+ }
14798
+ } else {
14799
+ setValue(rl.line);
14800
+ setError(void 0);
14801
+ }
14802
+ });
14803
+ const message = theme.style.message(config.message, status);
14804
+ let formattedValue = "";
14805
+ let helpTip;
14806
+ if (config.mask) {
14807
+ const maskChar = typeof config.mask === "string" ? config.mask : "*";
14808
+ formattedValue = maskChar.repeat(value.length);
14809
+ } else if (status !== "done") {
14810
+ helpTip = `${theme.style.help(theme.style.maskedText)}${cursorHide}`;
14811
+ }
14812
+ if (status === "done") {
14813
+ formattedValue = theme.style.answer(formattedValue);
14814
+ }
14815
+ let error = "";
14816
+ if (errorMsg) {
14817
+ error = theme.style.error(errorMsg);
14818
+ }
14819
+ return [[prefix, message, config.mask ? formattedValue : helpTip].join(" "), error];
14820
+ });
14821
+
14764
14822
  // node_modules/@inquirer/select/dist/index.js
14765
14823
  import { styleText as styleText4 } from "util";
14766
14824
  var selectTheme = {
@@ -14806,7 +14864,7 @@ function normalizeChoices2(choices) {
14806
14864
  return normalizedChoice;
14807
14865
  });
14808
14866
  }
14809
- var dist_default7 = createPrompt((config, done) => {
14867
+ var dist_default8 = createPrompt((config, done) => {
14810
14868
  const { loop = true, pageSize = 7 } = config;
14811
14869
  const theme = makeTheme(selectTheme, config.theme);
14812
14870
  const { keybindings } = theme;
@@ -15093,9 +15151,12 @@ function getPackageRoot(importMetaUrl) {
15093
15151
  current = parent;
15094
15152
  }
15095
15153
  }
15154
+ function computeBytesHash(content) {
15155
+ return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
15156
+ }
15096
15157
  function computeSourceHash(filePath) {
15097
15158
  const content = fs2.readFileSync(filePath);
15098
- return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
15159
+ return computeBytesHash(content);
15099
15160
  }
15100
15161
  function formatProvenanceMarker(format, source, hash, target) {
15101
15162
  if (format === "markdown") {
@@ -15404,6 +15465,11 @@ var EXIT_ERROR_SEVERITY_LOSS = 3;
15404
15465
  var EXIT_TARGET_NOT_DETECTED = 4;
15405
15466
  var EXIT_CONFIG_MISSING = 10;
15406
15467
  var EXIT_USER_INTERRUPT = 130;
15468
+ var EXIT_AUTH_REQUIRED = 5;
15469
+ var EXIT_REMOTE_UNAVAILABLE = 6;
15470
+ var EXIT_INTEGRITY_FAILURE = 7;
15471
+ var EXIT_OFFLINE_CACHE_MISS = 8;
15472
+ var EXIT_INCOMPATIBLE_VERSION = 9;
15407
15473
  var ConflictError = class extends Error {
15408
15474
  constructor(message) {
15409
15475
  super(message);
@@ -15416,6 +15482,44 @@ var TargetNotDetectedError = class extends Error {
15416
15482
  this.name = "TargetNotDetectedError";
15417
15483
  }
15418
15484
  };
15485
+ var AuthenticationError = class extends Error {
15486
+ constructor(message) {
15487
+ super(message);
15488
+ this.name = "AuthenticationError";
15489
+ }
15490
+ };
15491
+ var RemoteUnavailableError = class extends Error {
15492
+ constructor(message) {
15493
+ super(message);
15494
+ this.name = "RemoteUnavailableError";
15495
+ }
15496
+ };
15497
+ var IntegrityError = class extends Error {
15498
+ constructor(message) {
15499
+ super(message);
15500
+ this.name = "IntegrityError";
15501
+ }
15502
+ };
15503
+ var OfflineCacheMissError = class extends Error {
15504
+ constructor(message) {
15505
+ super(message);
15506
+ this.name = "OfflineCacheMissError";
15507
+ }
15508
+ };
15509
+ var IncompatibleVersionError = class extends Error {
15510
+ constructor(message) {
15511
+ super(message);
15512
+ this.name = "IncompatibleVersionError";
15513
+ }
15514
+ };
15515
+ var CredentialsLegacyError = class extends Error {
15516
+ constructor() {
15517
+ super(
15518
+ "Legacy single-key credential file detected. The credential-store layout changed to store a client ID/secret pair (the credential-pair format). Run 'tangyr auth login' to re-authenticate and store the new format."
15519
+ );
15520
+ this.name = "CredentialsLegacyError";
15521
+ }
15522
+ };
15419
15523
  function handleFatalError(err, logger) {
15420
15524
  const error = err instanceof Error ? err : new Error(String(err));
15421
15525
  logger.error(error.message);
@@ -15428,6 +15532,24 @@ function handleFatalError(err, logger) {
15428
15532
  if (error instanceof TargetNotDetectedError) {
15429
15533
  process.exit(EXIT_TARGET_NOT_DETECTED);
15430
15534
  }
15535
+ if (error instanceof AuthenticationError) {
15536
+ process.exit(EXIT_AUTH_REQUIRED);
15537
+ }
15538
+ if (error instanceof CredentialsLegacyError) {
15539
+ process.exit(EXIT_AUTH_REQUIRED);
15540
+ }
15541
+ if (error instanceof RemoteUnavailableError) {
15542
+ process.exit(EXIT_REMOTE_UNAVAILABLE);
15543
+ }
15544
+ if (error instanceof IntegrityError) {
15545
+ process.exit(EXIT_INTEGRITY_FAILURE);
15546
+ }
15547
+ if (error instanceof OfflineCacheMissError) {
15548
+ process.exit(EXIT_OFFLINE_CACHE_MISS);
15549
+ }
15550
+ if (error instanceof IncompatibleVersionError) {
15551
+ process.exit(EXIT_INCOMPATIBLE_VERSION);
15552
+ }
15431
15553
  process.exit(EXIT_FAILURE);
15432
15554
  }
15433
15555
  function exitOnErrorSeverityLoss(report, logger, opts = {}) {
@@ -15444,6 +15566,28 @@ function exitOnErrorSeverityLoss(report, logger, opts = {}) {
15444
15566
  }
15445
15567
  }
15446
15568
 
15569
+ // src/core/runtime-guard.ts
15570
+ var MINIMUM_SUPPORTED_NODE_MAJOR = 22;
15571
+ function checkNodeRuntime(nodeVersion) {
15572
+ const match = /^v?(\d+)\./.exec(nodeVersion);
15573
+ const major = match ? Number.parseInt(match[1], 10) : Number.NaN;
15574
+ if (Number.isNaN(major) || major < MINIMUM_SUPPORTED_NODE_MAJOR) {
15575
+ return {
15576
+ supported: false,
15577
+ message: `tangyr requires Node.js >=${MINIMUM_SUPPORTED_NODE_MAJOR}. Detected ${nodeVersion}. Install Node.js ${MINIMUM_SUPPORTED_NODE_MAJOR} or later (e.g. via nvm: "nvm install ${MINIMUM_SUPPORTED_NODE_MAJOR}") and retry.`
15578
+ };
15579
+ }
15580
+ return { supported: true };
15581
+ }
15582
+ function assertSupportedNodeRuntime(nodeVersion = process.version) {
15583
+ const result = checkNodeRuntime(nodeVersion);
15584
+ if (!result.supported) {
15585
+ process.stderr.write(`${result.message}
15586
+ `);
15587
+ process.exit(EXIT_FAILURE);
15588
+ }
15589
+ }
15590
+
15447
15591
  // src/commands/assess.ts
15448
15592
  import fs11 from "fs";
15449
15593
  import path12 from "path";
@@ -16926,63 +17070,364 @@ function resolveKitPath(config, configDir) {
16926
17070
  import fs12 from "fs";
16927
17071
  import os3 from "os";
16928
17072
  import path13 from "path";
17073
+
17074
+ // src/core/transport.ts
17075
+ var KIT_CONTRACT_VERSION = "1";
17076
+ var CONTRACT_VERSION_HEADER = "X-Tangyr-Contract-Version";
17077
+ var ACCESS_CLIENT_ID_HEADER = "CF-Access-Client-Id";
17078
+ var ACCESS_CLIENT_SECRET_HEADER = "CF-Access-Client-Secret";
17079
+ var REDACTED = "[redacted]";
17080
+ function assertHttps(originUrl) {
17081
+ const parsed = new URL(originUrl);
17082
+ if (parsed.protocol !== "https:") {
17083
+ throw new IntegrityError(
17084
+ `KitTransport rejected a non-HTTPS origin "${originUrl}" \u2014 only https:// origins are permitted (REQ-INT-006).`
17085
+ );
17086
+ }
17087
+ }
17088
+ async function performRequest(originUrl, requestPath, credentials, logger) {
17089
+ assertHttps(originUrl);
17090
+ const headers = {
17091
+ [ACCESS_CLIENT_ID_HEADER]: credentials.clientId,
17092
+ [ACCESS_CLIENT_SECRET_HEADER]: credentials.clientSecret,
17093
+ [CONTRACT_VERSION_HEADER]: KIT_CONTRACT_VERSION
17094
+ };
17095
+ logger?.verbose(
17096
+ `KitTransport GET ${originUrl}${requestPath} \u2014 headers: ${ACCESS_CLIENT_ID_HEADER}=${REDACTED}, ${ACCESS_CLIENT_SECRET_HEADER}=${REDACTED}, ${CONTRACT_VERSION_HEADER}=${KIT_CONTRACT_VERSION}`
17097
+ );
17098
+ const response = await fetch(`${originUrl}${requestPath}`, {
17099
+ method: "GET",
17100
+ redirect: "manual",
17101
+ headers
17102
+ });
17103
+ if (response.status >= 300 && response.status < 400) {
17104
+ throw new IntegrityError(
17105
+ `KitTransport rejected a redirect response (HTTP ${response.status}) from ${originUrl}${requestPath} \u2014 redirects are never followed; every redirect is rejected fail-closed (REQ-INT-006, resolved reject-all-redirects policy).`
17106
+ );
17107
+ }
17108
+ if (response.status === 426) {
17109
+ throw new IncompatibleVersionError(
17110
+ `The origin rejected this CLI's contract version (HTTP 426) at ${originUrl}${requestPath}. Upgrade tangyr to the latest release and retry.`
17111
+ );
17112
+ }
17113
+ return response;
17114
+ }
17115
+ function createFetchKitTransport(options = {}) {
17116
+ return {
17117
+ async probe(originUrl, credentials) {
17118
+ const response = await performRequest(
17119
+ originUrl,
17120
+ "/v1/probe",
17121
+ credentials,
17122
+ options.logger
17123
+ );
17124
+ const contentType = response.headers.get("content-type") ?? "";
17125
+ const metadata = contentType.includes("application/json") ? await response.json() : {};
17126
+ return { status: response.status, metadata };
17127
+ },
17128
+ async fetchObject(originUrl, objectPath, credentials) {
17129
+ const response = await performRequest(
17130
+ originUrl,
17131
+ objectPath,
17132
+ credentials,
17133
+ options.logger
17134
+ );
17135
+ const body = Buffer.from(await response.arrayBuffer());
17136
+ return { status: response.status, body };
17137
+ }
17138
+ };
17139
+ }
17140
+
17141
+ // src/commands/auth.ts
16929
17142
  var CREDENTIALS_DIR = path13.join(os3.homedir(), ".tangyr");
16930
17143
  var CREDENTIALS_PATH = path13.join(CREDENTIALS_DIR, "credentials.json");
16931
- async function runAuthLoginCommand(options, logger) {
16932
- let key = options.key;
16933
- if (!key) {
16934
- key = await dist_default6({ message: "Enter your Tangyr license key:" });
17144
+ function isCurrentCredentialRecord(value) {
17145
+ if (typeof value !== "object" || value === null) {
17146
+ return false;
16935
17147
  }
16936
- if (!key || key.trim().length === 0) {
16937
- logger.error("No license key provided.");
16938
- process.exit(1);
17148
+ const record = value;
17149
+ return record.schema === 2 && typeof record.clientId === "string" && record.clientId.length > 0 && typeof record.clientSecret === "string" && record.clientSecret.length > 0 && (record.validation === "validated" || record.validation === "unvalidated") && typeof record.storedAt === "string";
17150
+ }
17151
+ function readStoredCredentials() {
17152
+ if (!fs12.existsSync(CREDENTIALS_PATH)) {
17153
+ return null;
16939
17154
  }
16940
- fs12.mkdirSync(CREDENTIALS_DIR, { recursive: true });
16941
- const credentials = {
16942
- key: key.trim(),
16943
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
16944
- };
16945
- fs12.writeFileSync(
16946
- CREDENTIALS_PATH,
16947
- `${JSON.stringify(credentials, null, 2)}
16948
- `,
16949
- { mode: 384 }
16950
- );
17155
+ const raw = fs12.readFileSync(CREDENTIALS_PATH, "utf8");
17156
+ const parsed = JSON.parse(raw);
17157
+ if (!isCurrentCredentialRecord(parsed)) {
17158
+ throw new CredentialsLegacyError();
17159
+ }
17160
+ return parsed;
17161
+ }
17162
+ function ensureCredentialsDir(logger) {
16951
17163
  try {
16952
- fs12.chmodSync(CREDENTIALS_PATH, 384);
17164
+ fs12.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 448 });
17165
+ } catch (err) {
17166
+ if (err.code !== "EEXIST") {
17167
+ throw err;
17168
+ }
17169
+ }
17170
+ if (fs12.lstatSync(CREDENTIALS_DIR).isSymbolicLink()) {
17171
+ throw new Error(
17172
+ `Refusing to write credentials: "${CREDENTIALS_DIR}" already exists as a symbolic link, not a directory. Remove it and run 'tangyr auth login' again.`
17173
+ );
17174
+ }
17175
+ try {
17176
+ fs12.chmodSync(CREDENTIALS_DIR, 448);
16953
17177
  } catch {
17178
+ logger.warn(
17179
+ "Could not set owner-only (0700) permissions on the credentials directory on this platform; at-rest protection is reduced (REQ-AUT-007)."
17180
+ );
16954
17181
  }
16955
- const masked = `${key.trim().slice(0, 4)}...${key.trim().slice(-4)}`;
16956
- logger.info(`License key stored (${masked}).`);
16957
- logger.info(
16958
- "Remote validation is not yet implemented \u2014 key stored locally only."
17182
+ }
17183
+ function writeStoredCredentials(record, logger) {
17184
+ ensureCredentialsDir(logger);
17185
+ const content = `${JSON.stringify(record, null, 2)}
17186
+ `;
17187
+ const tmpPath = `${CREDENTIALS_PATH}.tmp`;
17188
+ try {
17189
+ fs12.writeFileSync(tmpPath, content, { mode: 384, flag: "wx" });
17190
+ } catch (err) {
17191
+ if (err.code === "EEXIST") {
17192
+ fs12.unlinkSync(tmpPath);
17193
+ fs12.writeFileSync(tmpPath, content, { mode: 384, flag: "wx" });
17194
+ } else {
17195
+ throw err;
17196
+ }
17197
+ }
17198
+ try {
17199
+ fs12.renameSync(tmpPath, CREDENTIALS_PATH);
17200
+ } catch (renameErr) {
17201
+ try {
17202
+ try {
17203
+ const destStat = fs12.lstatSync(CREDENTIALS_PATH);
17204
+ if (destStat.isSymbolicLink()) {
17205
+ fs12.unlinkSync(CREDENTIALS_PATH);
17206
+ }
17207
+ } catch {
17208
+ }
17209
+ fs12.copyFileSync(tmpPath, CREDENTIALS_PATH);
17210
+ try {
17211
+ fs12.chmodSync(CREDENTIALS_PATH, 384);
17212
+ } catch {
17213
+ logger.warn(
17214
+ "Could not set owner-only (0600) permissions on the credentials file on this platform; at-rest protection is reduced (REQ-AUT-007)."
17215
+ );
17216
+ }
17217
+ try {
17218
+ fs12.unlinkSync(tmpPath);
17219
+ } catch {
17220
+ }
17221
+ } catch (copyErr) {
17222
+ try {
17223
+ fs12.unlinkSync(tmpPath);
17224
+ } catch {
17225
+ }
17226
+ throw new Error(
17227
+ `writeStoredCredentials: failed to write ${CREDENTIALS_PATH} \u2014 ${String(renameErr)} / fallback copy failed \u2014 ${String(copyErr)}`
17228
+ );
17229
+ }
17230
+ }
17231
+ }
17232
+ var ADVANCE_EXPIRY_WARNING_DAYS = 30;
17233
+ var DEFINITIVE_DENIAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
17234
+ async function resolveCredentialForLogin(options, logger) {
17235
+ const envClientId = process.env.TANGYR_ACCESS_CLIENT_ID?.trim();
17236
+ const envClientSecret = process.env.TANGYR_ACCESS_CLIENT_SECRET?.trim();
17237
+ if (envClientId && envClientSecret) {
17238
+ logger.verbose(
17239
+ "Resolved credential from TANGYR_ACCESS_CLIENT_ID / TANGYR_ACCESS_CLIENT_SECRET."
17240
+ );
17241
+ return {
17242
+ pair: { clientId: envClientId, clientSecret: envClientSecret },
17243
+ source: "env"
17244
+ };
17245
+ }
17246
+ if (envClientId || envClientSecret) {
17247
+ throw new AuthenticationError(
17248
+ "Both TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET must be set together. Set both environment variables, or unset both and run 'tangyr auth login' interactively."
17249
+ );
17250
+ }
17251
+ const stored = readStoredCredentials();
17252
+ if (stored) {
17253
+ logger.verbose("Resolved credential from the stored credential file.");
17254
+ return {
17255
+ pair: { clientId: stored.clientId, clientSecret: stored.clientSecret },
17256
+ source: "stored"
17257
+ };
17258
+ }
17259
+ if (options.nonInteractive) {
17260
+ throw new AuthenticationError(
17261
+ "No credential available under --non-interactive. Set TANGYR_ACCESS_CLIENT_ID and TANGYR_ACCESS_CLIENT_SECRET, or drop --non-interactive and run 'tangyr auth login' again from an interactive terminal."
17262
+ );
17263
+ }
17264
+ const clientId = await dist_default6({
17265
+ message: "Enter your Tangyr access client ID:"
17266
+ });
17267
+ const clientSecret = await dist_default7({
17268
+ message: "Enter your Tangyr access client secret:"
17269
+ });
17270
+ if (clientId.trim().length === 0 || clientSecret.trim().length === 0) {
17271
+ throw new AuthenticationError("No credential provided.");
17272
+ }
17273
+ return {
17274
+ pair: { clientId: clientId.trim(), clientSecret: clientSecret.trim() },
17275
+ source: "interactive"
17276
+ };
17277
+ }
17278
+ function resolveOriginUrl(configPath) {
17279
+ let config;
17280
+ try {
17281
+ ({ config } = resolveConfig(configPath));
17282
+ } catch (err) {
17283
+ if (err instanceof ConfigNotFoundError) {
17284
+ return void 0;
17285
+ }
17286
+ throw err;
17287
+ }
17288
+ const source = config.source;
17289
+ return typeof source.url === "string" && source.url.length > 0 ? source.url : void 0;
17290
+ }
17291
+ async function validateAgainstOrigin(originUrl, pair, transport, logger) {
17292
+ if (!originUrl) {
17293
+ logger.verbose(
17294
+ "No remote origin configured (source.url) \u2014 treating as unreachable for validation purposes."
17295
+ );
17296
+ return { outcome: "unreachable" };
17297
+ }
17298
+ try {
17299
+ const result = await transport.probe(originUrl, pair);
17300
+ if (DEFINITIVE_DENIAL_STATUSES.has(result.status)) {
17301
+ return { outcome: "denied" };
17302
+ }
17303
+ if (result.status >= 200 && result.status < 300) {
17304
+ return { outcome: "success", metadata: result.metadata };
17305
+ }
17306
+ return { outcome: "unreachable" };
17307
+ } catch (err) {
17308
+ if (err instanceof IntegrityError || err instanceof IncompatibleVersionError) {
17309
+ throw err;
17310
+ }
17311
+ logger.verbose(
17312
+ `Origin probe failed: ${err instanceof Error ? err.message : String(err)}`
17313
+ );
17314
+ return { outcome: "unreachable" };
17315
+ }
17316
+ }
17317
+ async function runAuthLoginCommand(options, logger, transport = createFetchKitTransport({ logger })) {
17318
+ const resolved = await resolveCredentialForLogin(options, logger);
17319
+ if (options.skipValidation) {
17320
+ writeStoredCredentials(
17321
+ {
17322
+ schema: 2,
17323
+ clientId: resolved.pair.clientId,
17324
+ clientSecret: resolved.pair.clientSecret,
17325
+ validation: "unvalidated",
17326
+ storedAt: (/* @__PURE__ */ new Date()).toISOString()
17327
+ },
17328
+ logger
17329
+ );
17330
+ logger.info(
17331
+ "Credential stored (validation skipped via --skip-validation)."
17332
+ );
17333
+ logger.warn(
17334
+ "Validation state: stored, unvalidated. It will be revalidated on the next command that reaches the configured origin."
17335
+ );
17336
+ return;
17337
+ }
17338
+ const originUrl = resolveOriginUrl(options.config);
17339
+ const outcome = await validateAgainstOrigin(
17340
+ originUrl,
17341
+ resolved.pair,
17342
+ transport,
17343
+ logger
16959
17344
  );
17345
+ if (outcome.outcome === "denied") {
17346
+ throw new AuthenticationError(
17347
+ "The configured origin denied this credential. The credential was not stored. Obtain a valid client ID/secret pair and run 'tangyr auth login' again."
17348
+ );
17349
+ }
17350
+ writeStoredCredentials(
17351
+ {
17352
+ schema: 2,
17353
+ clientId: resolved.pair.clientId,
17354
+ clientSecret: resolved.pair.clientSecret,
17355
+ validation: outcome.outcome === "success" ? "validated" : "unvalidated",
17356
+ storedAt: (/* @__PURE__ */ new Date()).toISOString()
17357
+ },
17358
+ logger
17359
+ );
17360
+ if (outcome.outcome === "success") {
17361
+ logger.info("Credential validated and stored (stored, validated).");
17362
+ } else {
17363
+ logger.warn(
17364
+ "Could not reach the configured origin to validate this credential. Credential stored as stored, unvalidated \u2014 it will be revalidated on the next command that reaches the origin."
17365
+ );
17366
+ }
16960
17367
  }
16961
- function runAuthStatusCommand(_options, logger) {
16962
- if (!fs12.existsSync(CREDENTIALS_PATH)) {
17368
+ async function runAuthStatusCommand(options, logger, transport = createFetchKitTransport({ logger })) {
17369
+ const stored = readStoredCredentials();
17370
+ if (!stored) {
16963
17371
  logger.info(
16964
- "Not authenticated. Run 'tangyr auth login' to configure a license key."
17372
+ "Not authenticated. Run 'tangyr auth login' to configure a credential."
16965
17373
  );
16966
17374
  return;
16967
17375
  }
16968
- try {
16969
- const raw = fs12.readFileSync(CREDENTIALS_PATH, "utf8");
16970
- const credentials = JSON.parse(raw);
16971
- const masked = `${credentials.key.slice(0, 4)}...${credentials.key.slice(-4)}`;
16972
- logger.info(`Authenticated: ${masked}`);
16973
- logger.info(`Stored at: ${credentials.storedAt}`);
16974
- logger.info("Remote validation: not yet implemented.");
16975
- } catch {
16976
- logger.error("Failed to read credentials file. It may be corrupted.");
17376
+ const originUrl = resolveOriginUrl(options.config);
17377
+ const outcome = await validateAgainstOrigin(
17378
+ originUrl,
17379
+ { clientId: stored.clientId, clientSecret: stored.clientSecret },
17380
+ transport,
17381
+ logger
17382
+ );
17383
+ if (outcome.outcome === "denied") {
17384
+ throw new AuthenticationError(
17385
+ "The configured origin denied the stored credential \u2014 it has likely been revoked. Run 'tangyr auth login' with a new credential."
17386
+ );
17387
+ }
17388
+ if (outcome.outcome === "unreachable") {
17389
+ logger.info(`Credential status: stored, ${stored.validation}.`);
17390
+ logger.info(
17391
+ "The configured origin was not reached \u2014 label and expiry are not reported."
17392
+ );
17393
+ return;
17394
+ }
17395
+ if (stored.validation === "unvalidated") {
17396
+ writeStoredCredentials(
17397
+ {
17398
+ ...stored,
17399
+ validation: "validated",
17400
+ storedAt: (/* @__PURE__ */ new Date()).toISOString()
17401
+ },
17402
+ logger
17403
+ );
17404
+ }
17405
+ logger.info("Credential status: stored, validated.");
17406
+ const label = outcome.metadata.label;
17407
+ if (typeof label === "string" && label.length > 0) {
17408
+ logger.info(`Label: ${label}`);
17409
+ }
17410
+ const expiresAtRaw = outcome.metadata.expiresAt;
17411
+ if (typeof expiresAtRaw === "string") {
17412
+ const expiresAt = new Date(expiresAtRaw);
17413
+ if (!Number.isNaN(expiresAt.getTime())) {
17414
+ logger.info(`Expires: ${expiresAt.toISOString()}`);
17415
+ const daysUntilExpiry = (expiresAt.getTime() - Date.now()) / (24 * 60 * 60 * 1e3);
17416
+ if (daysUntilExpiry <= ADVANCE_EXPIRY_WARNING_DAYS) {
17417
+ logger.warn(
17418
+ `Credential expires in ${Math.max(0, Math.ceil(daysUntilExpiry))} day(s) (${expiresAt.toISOString()}) \u2014 renew it soon.`
17419
+ );
17420
+ }
17421
+ }
16977
17422
  }
16978
17423
  }
16979
17424
  function runAuthLogoutCommand(_options, logger) {
16980
17425
  if (!fs12.existsSync(CREDENTIALS_PATH)) {
16981
- logger.info("No credentials found. Nothing to remove.");
17426
+ logger.info("No credential found. Nothing to remove.");
16982
17427
  return;
16983
17428
  }
16984
17429
  fs12.unlinkSync(CREDENTIALS_PATH);
16985
- logger.info("License key removed.");
17430
+ logger.info("Credential removed.");
16986
17431
  }
16987
17432
 
16988
17433
  // src/commands/cleanup.ts
@@ -21698,14 +22143,14 @@ ${JSON.stringify(config, null, 2)}`);
21698
22143
  checked: enabled
21699
22144
  }))
21700
22145
  });
21701
- const selectedConflict = await dist_default7({
22146
+ const selectedConflict = await dist_default8({
21702
22147
  message: "Conflict policy",
21703
22148
  choices: ["ask", "skip", "backup", "overwrite"].map((value) => ({
21704
22149
  name: value,
21705
22150
  value
21706
22151
  }))
21707
22152
  });
21708
- const selectedVerbosity = await dist_default7({
22153
+ const selectedVerbosity = await dist_default8({
21709
22154
  message: "Verbosity",
21710
22155
  choices: ["quiet", "normal", "verbose"].map((value) => ({
21711
22156
  name: value,
@@ -21777,9 +22222,17 @@ var bundledCopilotManifestPathsByPlatform = {
21777
22222
  };
21778
22223
  function detectTargets(logger) {
21779
22224
  const result = {
21780
- "claude-code": commandExists(process.platform === "win32" ? "where.exe" : "which", ["claude"], logger),
22225
+ "claude-code": commandExists(
22226
+ process.platform === "win32" ? "where.exe" : "which",
22227
+ ["claude"],
22228
+ logger
22229
+ ),
21781
22230
  copilot: detectsCopilot(logger),
21782
- codex: commandExists(process.platform === "win32" ? "where.exe" : "which", ["codex"], logger)
22231
+ codex: commandExists(
22232
+ process.platform === "win32" ? "where.exe" : "which",
22233
+ ["codex"],
22234
+ logger
22235
+ )
21783
22236
  };
21784
22237
  return result;
21785
22238
  }
@@ -21809,9 +22262,16 @@ function detectCopilot(logger, dependencies = {}) {
21809
22262
  ...bundledCopilotEditorCommandsByPlatform[platform] ?? []
21810
22263
  ];
21811
22264
  for (const command of commandCandidates) {
21812
- const result = spawnSyncImpl(command, ["--list-extensions", "--show-versions"]);
22265
+ const result = spawnSyncImpl(command, [
22266
+ "--list-extensions",
22267
+ "--show-versions"
22268
+ ]);
21813
22269
  if (result.error) {
21814
- logDetectionError(`${command} --list-extensions --show-versions`, result.error, logger);
22270
+ logDetectionError(
22271
+ `${command} --list-extensions --show-versions`,
22272
+ result.error,
22273
+ logger
22274
+ );
21815
22275
  continue;
21816
22276
  }
21817
22277
  if (result.status !== 0) {
@@ -21892,10 +22352,14 @@ function commandExists(command, args, logger) {
21892
22352
  }
21893
22353
  function logDetectionError(attemptedCommand, error, logger) {
21894
22354
  if (error.code === "ENOENT") {
21895
- logger?.verbose(`${attemptedCommand}: executable not found while detecting target`);
22355
+ logger?.verbose(
22356
+ `${attemptedCommand}: executable not found while detecting target`
22357
+ );
21896
22358
  return;
21897
22359
  }
21898
- logger?.verbose(`${attemptedCommand} failed: ${error.message ?? "unknown spawn error"}`);
22360
+ logger?.verbose(
22361
+ `${attemptedCommand} failed: ${error.message ?? "unknown spawn error"}`
22362
+ );
21899
22363
  }
21900
22364
 
21901
22365
  // src/commands/doctor.ts
@@ -22210,7 +22674,7 @@ async function runInitCommand(_options, logger) {
22210
22674
  checked: detected
22211
22675
  }))
22212
22676
  });
22213
- const onConflict = await dist_default7({
22677
+ const onConflict = await dist_default8({
22214
22678
  message: "Conflict policy",
22215
22679
  choices: ["ask", "skip", "backup", "overwrite"].map((value) => ({
22216
22680
  name: value,
@@ -26207,7 +26671,7 @@ async function runInstallCommand(options, logger) {
26207
26671
  );
26208
26672
  process.exit(1);
26209
26673
  } else {
26210
- action = await dist_default7({
26674
+ action = await dist_default8({
26211
26675
  message: "Existing installation found. Choose an action:",
26212
26676
  choices: [
26213
26677
  {
@@ -26336,7 +26800,7 @@ Found ${conflictingFindings.length} conflicting target(s). Please choose an acti
26336
26800
  `
26337
26801
  );
26338
26802
  for (const finding of conflictingFindings) {
26339
- const choice = await dist_default7({
26803
+ const choice = await dist_default8({
26340
26804
  message: `Conflict: ${finding.path} (${finding.tool})
26341
26805
  Choose an action:`,
26342
26806
  choices: [
@@ -30081,6 +30545,7 @@ var commandNames = [
30081
30545
  "probe"
30082
30546
  ];
30083
30547
  async function main() {
30548
+ assertSupportedNodeRuntime();
30084
30549
  process.on("SIGINT", () => {
30085
30550
  process.exit(EXIT_USER_INTERRUPT);
30086
30551
  });
@@ -30111,6 +30576,9 @@ function buildProgram() {
30111
30576
  ).option(
30112
30577
  "-y, --yes",
30113
30578
  "Skip interactive confirmation and apply non-interactive conflict handling"
30579
+ ).option(
30580
+ "--non-interactive",
30581
+ "Never prompt; fail closed when a required input is missing (auth login)"
30114
30582
  ).option("-v, --verbose", "Emit detailed output").option("-q, --quiet", "Emit errors only").option("--no-color", "Disable colorized output").option(
30115
30583
  "--claude-config-dir <path>",
30116
30584
  "Claude Code global root directory (repeatable; overrides claudeCodeGlobalPaths in config)",
@@ -30137,19 +30605,23 @@ function buildProgram() {
30137
30605
  );
30138
30606
  }
30139
30607
  );
30140
- const authCmd = program2.command("auth").description("License key management");
30141
- authCmd.command("login").description("Authenticate with a license key").option("--key <key>", "License key").action(async (options) => {
30608
+ const authCmd = program2.command("auth").description("Credential management");
30609
+ authCmd.command("login").description("Authenticate with a client ID/secret credential pair").option(
30610
+ "--skip-validation",
30611
+ "Store the credential without contacting the configured origin"
30612
+ ).action(async (options) => {
30142
30613
  const context = commandContext(program2);
30614
+ const nonInteractive = context.options.nonInteractive === true || !process.stdin.isTTY;
30143
30615
  await runAuthLoginCommand(
30144
- { ...context.options, ...options },
30616
+ { ...context.options, ...options, nonInteractive },
30145
30617
  context.logger
30146
30618
  );
30147
30619
  });
30148
- authCmd.command("status").description("Show current authentication status").action(() => {
30620
+ authCmd.command("status").description("Show current authentication status").action(async () => {
30149
30621
  const context = commandContext(program2);
30150
- runAuthStatusCommand({ ...context.options }, context.logger);
30622
+ await runAuthStatusCommand({ ...context.options }, context.logger);
30151
30623
  });
30152
- authCmd.command("logout").description("Remove stored license key").action(() => {
30624
+ authCmd.command("logout").description("Remove stored credential").action(() => {
30153
30625
  const context = commandContext(program2);
30154
30626
  runAuthLogoutCommand({ ...context.options }, context.logger);
30155
30627
  });
@@ -30238,7 +30710,7 @@ function buildProgram() {
30238
30710
  return program2;
30239
30711
  }
30240
30712
  async function runInteractiveMenu(program2, logger) {
30241
- const choice = await dist_default7({
30713
+ const choice = await dist_default8({
30242
30714
  message: "Select a Tangyr command",
30243
30715
  choices: [
30244
30716
  { name: "Assess", value: "assess" },
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
6
9
  "type": "module",
7
10
  "bin": {
8
11
  "tangyr": "./dist/index.js"
@@ -33,9 +36,13 @@
33
36
  "bugs": {
34
37
  "url": "https://github.com/alessandroraffa/tangyr-cli/issues"
35
38
  },
39
+ "overrides": {
40
+ "esbuild": "^0.28.1"
41
+ },
36
42
  "devDependencies": {
37
43
  "@iarna/toml": "^2.2.5",
38
44
  "@inquirer/prompts": "^8.4.1",
45
+ "@types/node": "^22.20.1",
39
46
  "chalk": "^5.6.2",
40
47
  "commander": "^14.0.3",
41
48
  "execa": "^9.0.0",
@@ -46,6 +53,5 @@
46
53
  "typescript": "^5.0.0",
47
54
  "vitest": "^3.0.0",
48
55
  "yaml": "^2.8.3"
49
- },
50
- "dependencies": {}
56
+ }
51
57
  }