tines 0.0.150 → 0.0.152

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 (2) hide show
  1. package/dist/index.js +122 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3774,6 +3774,64 @@ function requirementFix(r, ref) {
3774
3774
  command: `tines issues artifacts delete ${ref} ${r.artifact} && ${attach(r.type ?? "file")}`
3775
3775
  };
3776
3776
  }
3777
+ function concreteContentType(ct) {
3778
+ return ct !== void 0 && ct.includes("/") && !ct.endsWith("/") ? ct : void 0;
3779
+ }
3780
+ function requirementAccepts(r, type, contentType) {
3781
+ if (r.type !== void 0 && r.type !== type) return false;
3782
+ if (r.content_type === void 0) return true;
3783
+ if (contentType === void 0) return true;
3784
+ return contentType.startsWith(r.content_type);
3785
+ }
3786
+ function declaredContentType(reqs, type) {
3787
+ if (type !== "text" && type !== "file") return void 0;
3788
+ const declared = [
3789
+ ...new Set(
3790
+ reqs.filter((r) => r.type === type).map((r) => concreteContentType(r.content_type)).filter((ct) => ct !== void 0)
3791
+ )
3792
+ ];
3793
+ return declared.length === 1 ? declared[0] : void 0;
3794
+ }
3795
+
3796
+ // ../shared/src/artifact-site.ts
3797
+ var ARTIFACT_SITE_LINK_TTL_MS = 60 * 60 * 1e3;
3798
+ var ARTIFACT_SITE_INDEX = "index.html";
3799
+ function siteEntry(type, contentType, files = []) {
3800
+ if (type === "file" || type === "text") {
3801
+ return (contentType ?? "").toLowerCase().startsWith("text/html") ? "" : null;
3802
+ }
3803
+ if (type === "folder") {
3804
+ return files.some((f) => f.path === ARTIFACT_SITE_INDEX) ? ARTIFACT_SITE_INDEX : null;
3805
+ }
3806
+ return null;
3807
+ }
3808
+ var VIEWPORT_META = /<meta[^>]+name\s*=\s*["']?viewport["']?[^>]*content\s*=\s*["'][^"']*width\s*=\s*device-width/i;
3809
+ var EXTERNAL_URL = /^(?:https?:)?\/\//i;
3810
+ function externalRefs(html) {
3811
+ const found = [];
3812
+ const push = (url) => {
3813
+ if (url && EXTERNAL_URL.test(url.trim()) && !found.includes(url.trim())) found.push(url.trim());
3814
+ };
3815
+ for (const m of html.matchAll(/<script[^>]*\ssrc\s*=\s*["']([^"']+)["']/gi)) push(m[1]);
3816
+ for (const m of html.matchAll(/<link[^>]*\shref\s*=\s*["']([^"']+)["']/gi)) push(m[1]);
3817
+ for (const m of html.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/gi)) push(m[1]);
3818
+ return found;
3819
+ }
3820
+ function lintHtmlArtifact(html) {
3821
+ const warnings = [];
3822
+ if (!VIEWPORT_META.test(html)) {
3823
+ warnings.push(
3824
+ 'no viewport meta tag \u2014 add <meta name="viewport" content="width=device-width, initial-scale=1"> so it lays out for phones'
3825
+ );
3826
+ }
3827
+ const external = externalRefs(html);
3828
+ if (external.length > 0) {
3829
+ warnings.push(
3830
+ `external scripts/styles are blocked by the sandbox policy (${external.slice(0, 3).join(", ")}${external.length > 3 ? ", \u2026" : ""}) \u2014 inline or vendor them into the artifact`
3831
+ );
3832
+ }
3833
+ return warnings;
3834
+ }
3777
3835
 
3778
3836
  // ../shared/src/client.ts
3779
3837
  var ApiError = class extends Error {
@@ -3957,6 +4015,11 @@ function createApiClient(options) {
3957
4015
  // Issue artifacts (name-addressed under the issue)
3958
4016
  listArtifacts: (issueId) => get(`/api/v1/issues/${issueId}/artifacts`),
3959
4017
  getArtifact: (issueId, name2) => get(artifactPath(issueId, name2)),
4018
+ /**
4019
+ * Mints a short-lived signed URL that renders an HTML artifact live
4020
+ * (422 `not_a_site` when the artifact is not HTML / has no index.html).
4021
+ */
4022
+ createArtifactSiteLink: (issueId, name2, body = {}) => request("POST", artifactPath(issueId, name2, "/site-link"), body),
3960
4023
  /** JSON upsert for text/link/pr: creates the artifact or appends a version. */
3961
4024
  putArtifact: (issueId, name2, body) => request("PUT", artifactPath(issueId, name2), body),
3962
4025
  /** Raw-body upload for `file`: creates the artifact or appends a version. */
@@ -4970,9 +5033,6 @@ function normalizePositional(raw) {
4970
5033
  return { value: raw, stdin: false };
4971
5034
  }
4972
5035
  var isUrl = (v) => /^https?:\/\//.test(v);
4973
- function concreteContentType(ct) {
4974
- return ct !== void 0 && ct.includes("/") && !ct.endsWith("/") ? ct : void 0;
4975
- }
4976
5036
  function declaredTypes(gates) {
4977
5037
  return [
4978
5038
  ...new Set(gates.map((g) => g.check.type).filter((t) => t !== void 0))
@@ -5000,13 +5060,6 @@ function joinTransitions(names) {
5000
5060
  function fixCommand(gate, ref) {
5001
5061
  return gate.check.fix || requirementFix(gate.check, ref).command;
5002
5062
  }
5003
- function accepts(gate, type, contentType) {
5004
- const check = gate.check;
5005
- if (check.type !== void 0 && check.type !== type) return false;
5006
- if (check.content_type === void 0) return true;
5007
- if (contentType === void 0) return true;
5008
- return contentType.startsWith(check.content_type);
5009
- }
5010
5063
  function effectiveContentType(type, source, explicit, sniff) {
5011
5064
  if (explicit !== void 0) return explicit;
5012
5065
  if (type === "text") return "text/markdown";
@@ -5124,13 +5177,10 @@ function planPositional(name2, raw, gates, probe, flags) {
5124
5177
  }
5125
5178
  }
5126
5179
  function gateContentType(gates, type) {
5127
- if (type !== "text" && type !== "file") return void 0;
5128
- const declared = [
5129
- ...new Set(
5130
- gates.filter((g) => g.check.type === type).map((g) => concreteContentType(g.check.content_type)).filter((ct) => ct !== void 0)
5131
- )
5132
- ];
5133
- return declared.length === 1 ? declared[0] : void 0;
5180
+ return declaredContentType(
5181
+ gates.map((g) => g.check),
5182
+ type
5183
+ );
5134
5184
  }
5135
5185
  function planFlags(flags, probe) {
5136
5186
  const ct = flags.contentType;
@@ -5183,7 +5233,7 @@ function checkExistingSlot(plan, gates, ref, name2, flags, positional) {
5183
5233
  }
5184
5234
  function checkAccepted(plan, gates, ref, name2, flags, positional, sniff) {
5185
5235
  const effective = effectiveContentType(plan.type, plan.source, plan.contentType, sniff);
5186
- if (gates.some((g2) => accepts(g2, plan.type, effective))) return;
5236
+ if (gates.some((g2) => requirementAccepts(g2.check, plan.type, effective))) return;
5187
5237
  const label = sourceLabel(flags, positional);
5188
5238
  const typeMatched = gates.filter((g2) => (g2.check.type ?? plan.type) === plan.type);
5189
5239
  if (typeMatched.length > 0) {
@@ -5202,7 +5252,11 @@ function satisfiedBy(artifact, gates) {
5202
5252
  const rejects = [];
5203
5253
  for (const gate of gates) {
5204
5254
  if (gate.check.artifact !== artifact.name) continue;
5205
- if (accepts(gate, artifact.artifact_type, artifact.current_version.content_type ?? void 0)) {
5255
+ if (requirementAccepts(
5256
+ gate.check,
5257
+ artifact.artifact_type,
5258
+ artifact.current_version.content_type ?? void 0
5259
+ )) {
5206
5260
  satisfies.push(gate.transition);
5207
5261
  } else {
5208
5262
  rejects.push({ transition: gate.transition, wants: wantsLabel(gate, artifact) });
@@ -5887,6 +5941,16 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
5887
5941
  `current: v${artifact.current_version.version} (${artifact.fresh ? "fresh" : "attached before the current state \u2014 reaffirm or attach a new version to satisfy gates"})`
5888
5942
  );
5889
5943
  console.log(`summary: ${artifactSummary(artifact)}`);
5944
+ const entry = siteEntry(
5945
+ artifact.artifact_type,
5946
+ artifact.current_version.content_type,
5947
+ artifact.current_version.files ?? []
5948
+ );
5949
+ if (entry !== null) {
5950
+ console.log(
5951
+ `site: renders live from /${entry} \u2014 "tines issues artifacts site-link ${issue.project_name}/${issue.number} ${artifact.name}" for a viewable URL`
5952
+ );
5953
+ }
5890
5954
  console.log("\nversions:");
5891
5955
  table(
5892
5956
  artifact.versions.map((v) => [
@@ -5936,10 +6000,13 @@ files (v${artifact.current_version.version}):`);
5936
6000
  );
5937
6001
  const withDescription = opts.description !== void 0 ? { description: opts.description } : {};
5938
6002
  let artifact;
6003
+ let siteHtml = null;
5939
6004
  if (plan.type === "folder") {
5940
6005
  const dir = plan.source.dir;
5941
6006
  const files = walkFolder(dir);
5942
6007
  if (files.length === 0) die(`${dir} contains no files to snapshot`);
6008
+ const index = files.find((f) => f.path === ARTIFACT_SITE_INDEX);
6009
+ if (index) siteHtml = index.bytes.toString("utf8");
5943
6010
  artifact = await api.uploadArtifactFolder(issue.id, name2, files);
5944
6011
  if (opts.description !== void 0) {
5945
6012
  artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
@@ -5957,15 +6024,18 @@ files (v${artifact.current_version.version}):`);
5957
6024
  die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
5958
6025
  }
5959
6026
  }
6027
+ const contentType = plan.contentType ?? (fromPath === null ? "application/octet-stream" : sniffContentType(fromPath));
6028
+ if (siteEntry("file", contentType) !== null) siteHtml = bytes.toString("utf8");
5960
6029
  artifact = await api.uploadArtifactFile(issue.id, name2, bytes, {
5961
6030
  filename: plan.filename ?? (fromPath === null ? name2 : basename(fromPath)),
5962
- contentType: plan.contentType ?? (fromPath === null ? "application/octet-stream" : sniffContentType(fromPath))
6031
+ contentType
5963
6032
  });
5964
6033
  if (opts.description !== void 0) {
5965
6034
  artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
5966
6035
  }
5967
6036
  } else if (plan.type === "text") {
5968
6037
  const content = readTextSource(plan.source);
6038
+ if (siteEntry("text", plan.contentType ?? null) !== null) siteHtml = content;
5969
6039
  artifact = await api.putArtifact(issue.id, name2, {
5970
6040
  type: "text",
5971
6041
  content,
@@ -5996,6 +6066,12 @@ files (v${artifact.current_version.version}):`);
5996
6066
  console.log(
5997
6067
  `attached "${artifact.name}" v${artifact.current_version.version} (${artifactTypeLabel(artifact)}) to ${issue.project_name}/${issue.number} \u2014 fresh${gateNote}`
5998
6068
  );
6069
+ if (siteHtml !== null) {
6070
+ console.log(
6071
+ `site: renders live \u2014 "tines issues artifacts site-link ${issue.project_name}/${issue.number} ${artifact.name}" for a viewable URL`
6072
+ );
6073
+ for (const warning of lintHtmlArtifact(siteHtml)) console.log(`warning: ${warning}`);
6074
+ }
5999
6075
  }
6000
6076
  );
6001
6077
  withCommon(
@@ -6011,6 +6087,32 @@ files (v${artifact.current_version.version}):`);
6011
6087
  `reaffirmed "${artifact.name}" on ${issue.project_name}/${issue.number}: v${artifact.current_version.version} reaffirms v${artifact.current_version.reaffirmed_from} \u2014 fresh as of now`
6012
6088
  );
6013
6089
  });
6090
+ withCommon(
6091
+ artifactsCmd.command("site-link <ref> <name>").description(
6092
+ "Mint a short-lived URL that renders an HTML artifact live (scripts running) \u2014 for screenshotting your own prototype"
6093
+ ).option(
6094
+ "--version <n>",
6095
+ "pin the link to a specific version (defaults to current)",
6096
+ (v) => Number.parseInt(v, 10)
6097
+ )
6098
+ ).action(async (ref, name2, opts) => {
6099
+ const api = client(opts);
6100
+ const issue = await resolveIssue(api, ref);
6101
+ const link = await api.createArtifactSiteLink(
6102
+ issue.id,
6103
+ name2,
6104
+ opts.version === void 0 ? {} : { version: opts.version }
6105
+ );
6106
+ if (opts.json) return printJson(link);
6107
+ console.log(link.url);
6108
+ const minutes = Math.max(1, Math.round((link.expires_at - Date.now()) / 6e4));
6109
+ console.log(`v${link.version}, expires in ~${minutes} minute${minutes === 1 ? "" : "s"}`);
6110
+ if (link.mode === "same-origin") {
6111
+ console.log(
6112
+ "mode: same-origin sandbox (opaque origin) \u2014 localStorage and cookies throw inside the page"
6113
+ );
6114
+ }
6115
+ });
6014
6116
  withCommon(
6015
6117
  artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option(
6016
6118
  "--version <n>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.150",
3
+ "version": "0.0.152",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",