@getmonoceros/workbench 1.29.1 → 1.30.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.
@@ -1,7 +1,7 @@
1
1
  id: atlassian
2
2
  category: feature
3
3
  displayName: Atlassian
4
- description: 'Rovo Dev (`acli rovodev`) and Teamwork Graph (`twg`) CLIs sharing one Atlassian account. Auth persists across rebuilds.'
4
+ description: 'Rovo Dev (`acli rovodev`), Teamwork Graph (`twg`) and Forge (`forge`) CLIs sharing one Atlassian account. Auth persists across rebuilds.'
5
5
  documentationURL: https://developer.atlassian.com/cloud/
6
6
  options:
7
7
  rovodev:
@@ -14,6 +14,11 @@ options:
14
14
  default: true
15
15
  description: 'Install twg (Teamwork Graph CLI).'
16
16
  surface: yml
17
+ forge:
18
+ type: boolean
19
+ default: true
20
+ description: 'Install the Forge CLI (`forge`) for building Atlassian apps.'
21
+ surface: yml
17
22
  instance:
18
23
  type: string
19
24
  default: ''
@@ -35,18 +40,36 @@ options:
35
40
  description: "Optional Bitbucket app password; only needed for twg's Bitbucket commands."
36
41
  surface: env
37
42
  feature:
38
- version: 1.1.0
43
+ version: 1.2.0
39
44
  persistentHomePaths: [.config/acli, .rovodev, .config/twg, .agents]
40
45
  vscodeExtensions: [Atlassian.atlascode]
46
+ # Forge is keychain-free in a container: instead of `forge login` (which
47
+ # needs libsecret), Atlassian's documented path is the FORGE_EMAIL /
48
+ # FORGE_API_TOKEN env vars, read at command time. We hand them to the
49
+ # workspace runtime env from the shared account options, gated on the
50
+ # `forge` toggle. Sibling of a service's connectionEnv (ADR 0021).
51
+ workspaceEnv:
52
+ - whenOption: forge
53
+ vars:
54
+ FORGE_EMAIL: '${email}'
55
+ FORGE_API_TOKEN: '${apiToken}'
41
56
  briefing:
42
57
  - whenOption: rovodev
43
58
  text: 'Atlassian Rovo Dev — invoke via `acli rovodev`. Pre-authenticated against the Atlassian account configured in the feature options (shared with twg).'
44
59
  - whenOption: twg
45
60
  text: 'Atlassian Teamwork Graph CLI (`twg`) — work-data access across Jira, Confluence, Bitbucket, JSM and Assets. Pre-authenticated. Sub-skills cover status rollups, engineering work, ops health and context discovery.'
61
+ - whenOption: forge
62
+ text: 'Atlassian Forge CLI (`forge`) for building and deploying Forge apps. Authenticated via FORGE_EMAIL / FORGE_API_TOKEN from the shared account (no `forge login` needed in the container).'
46
63
  presets:
47
64
  twg:
48
65
  rovodev: false
49
66
  twg: true
67
+ forge: false
50
68
  rovodev:
51
69
  rovodev: true
52
70
  twg: false
71
+ forge: false
72
+ forge:
73
+ rovodev: false
74
+ twg: false
75
+ forge: true
@@ -3,25 +3,31 @@
3
3
  #
4
4
  # Installs Atlassian CLIs that share a single Atlassian account:
5
5
  # - Rovo Dev (acli, default on) — the `acli rovodev` AI agent
6
- # - Teamwork Graph (twg, default off)
6
+ # - Teamwork Graph (twg, default on)
7
+ # - Forge (forge, default on) — the `@forge/cli` app-dev toolchain
7
8
  #
8
- # Each tool is toggled independently via the `rovodev` / `twg`
9
- # options. Credentials (instance / email / apiToken) are shared
10
- # across both tools when set; per-tool post-create hooks under
11
- # /usr/local/share/monoceros/post-create.d/ perform the
12
- # non-interactive login on first container start.
9
+ # Each tool is toggled independently via the `rovodev` / `twg` / `forge`
10
+ # options. Credentials (instance / email / apiToken) are shared across
11
+ # the tools when set; rovodev and twg get per-tool post-create hooks
12
+ # under /usr/local/share/monoceros/post-create.d/ that perform the
13
+ # non-interactive login on first container start. Forge has no login
14
+ # hook: it reads FORGE_EMAIL / FORGE_API_TOKEN from the workspace
15
+ # runtime env, which the workbench injects from the shared account
16
+ # options (feature.workspaceEnv) — the keychain-free path Atlassian
17
+ # recommends for containers.
13
18
 
14
19
  set -euo pipefail
15
20
 
16
21
  ROVODEV="${ROVODEV:-true}"
17
22
  TWG="${TWG:-true}"
23
+ FORGE="${FORGE:-true}"
18
24
  INSTANCE="${INSTANCE:-}"
19
25
  EMAIL="${EMAIL:-}"
20
26
  APITOKEN="${APITOKEN:-}"
21
27
  BITBUCKETTOKEN="${BITBUCKETTOKEN:-}"
22
28
 
23
- if [ "${ROVODEV}" != "true" ] && [ "${TWG}" != "true" ]; then
24
- echo "[atlassian] both rovodev and twg disabled — nothing to install" >&2
29
+ if [ "${ROVODEV}" != "true" ] && [ "${TWG}" != "true" ] && [ "${FORGE}" != "true" ]; then
30
+ echo "[atlassian] rovodev, twg and forge all disabled — nothing to install" >&2
25
31
  exit 0
26
32
  fi
27
33
 
@@ -185,4 +191,24 @@ EOF
185
191
  fi
186
192
  fi
187
193
 
194
+ # ─── Forge (@forge/cli) ───────────────────────────────────────────
195
+ if [ "${FORGE}" = "true" ]; then
196
+ # Forge is a Node CLI; the runtime ships Node, so a global npm install
197
+ # is enough. No post-create login hook: `forge login` stores creds in
198
+ # the OS keychain (libsecret on Linux), which we deliberately do not
199
+ # carry. Instead Forge reads FORGE_EMAIL / FORGE_API_TOKEN at command
200
+ # time; the workbench injects them into the workspace runtime env from
201
+ # this feature's `email` / `apiToken` options (feature.workspaceEnv),
202
+ # so the right account is in scope without a login step here.
203
+ echo "[atlassian/forge] installing @forge/cli globally via npm"
204
+ npm install -g @forge/cli
205
+ forge --version >/dev/null 2>&1 || {
206
+ echo "[atlassian/forge] ERROR: install completed but \`forge\` is not on PATH" >&2
207
+ exit 1
208
+ }
209
+ if [ -z "${EMAIL}" ] || [ -z "${APITOKEN}" ]; then
210
+ echo "[atlassian/forge] no email/apiToken set — set them in the container yml so FORGE_EMAIL/FORGE_API_TOKEN reach the shell"
211
+ fi
212
+ fi
213
+
188
214
  echo "[atlassian] done"
package/dist/bin.js CHANGED
@@ -665,7 +665,16 @@ var init_feature_doc = __esm({
665
665
 
666
666
  // src/catalog/descriptor.ts
667
667
  import { z as z2 } from "zod";
668
- var DESCRIPTOR_ID_RE, CategorySchema, OptionTypeSchema, SurfaceSchema, OptionValueSchema, OptionSpecSchema, BriefingLineSchema, HealthcheckSchema, LanguageBlockSchema, ServiceBlockSchema, PersistentHomeFileSchema, FeatureBlockSchema, DescriptorSchema;
668
+ function workspaceEnvTokens(vars) {
669
+ const tokens = [];
670
+ for (const template of Object.values(vars)) {
671
+ for (const m of template.matchAll(/\$\{([A-Za-z0-9_]+)\}/g)) {
672
+ tokens.push(m[1]);
673
+ }
674
+ }
675
+ return tokens;
676
+ }
677
+ var DESCRIPTOR_ID_RE, CategorySchema, OptionTypeSchema, SurfaceSchema, OptionValueSchema, OptionSpecSchema, BriefingLineSchema, HealthcheckSchema, LanguageBlockSchema, ServiceBlockSchema, PersistentHomeFileSchema, WorkspaceEnvBlockSchema, FeatureBlockSchema, DescriptorSchema;
669
678
  var init_descriptor = __esm({
670
679
  "src/catalog/descriptor.ts"() {
671
680
  "use strict";
@@ -766,12 +775,24 @@ var init_descriptor = __esm({
766
775
  path: z2.string().min(1),
767
776
  initialContent: z2.string().optional()
768
777
  });
778
+ WorkspaceEnvBlockSchema = z2.object({
779
+ whenOption: z2.string().optional(),
780
+ vars: z2.record(z2.string(), z2.string())
781
+ });
769
782
  FeatureBlockSchema = z2.object({
770
783
  /** Publishable feature version (devcontainer-feature.json `version`). */
771
784
  version: z2.string().min(1),
772
785
  persistentHomePaths: z2.array(z2.string().min(1)).optional(),
773
786
  persistentHomeFiles: z2.array(PersistentHomeFileSchema).optional(),
774
- vscodeExtensions: z2.array(z2.string()).optional()
787
+ vscodeExtensions: z2.array(z2.string()).optional(),
788
+ /**
789
+ * Named runtime env injected into the workspace container (compose
790
+ * `environment:` / image-mode `containerEnv`). Catalog/CLI-side only — not
791
+ * emitted into the published devcontainer-feature.json (like `presets`),
792
+ * because it drives how the workbench wires the container, not the feature
793
+ * install. See `featureWorkspaceEnv` in create/scaffold.ts.
794
+ */
795
+ workspaceEnv: z2.array(WorkspaceEnvBlockSchema).optional()
775
796
  });
776
797
  DescriptorSchema = z2.object({
777
798
  id: z2.string().regex(DESCRIPTOR_ID_RE, "id must be lowercase letters/digits/hyphens"),
@@ -833,6 +854,24 @@ var init_descriptor = __esm({
833
854
  });
834
855
  }
835
856
  });
857
+ data.feature?.workspaceEnv?.forEach((block, i) => {
858
+ if (block.whenOption !== void 0 && !optionKeys.has(block.whenOption)) {
859
+ ctx.addIssue({
860
+ code: z2.ZodIssueCode.custom,
861
+ path: ["feature", "workspaceEnv", i, "whenOption"],
862
+ message: `whenOption '${block.whenOption}' is not a declared option`
863
+ });
864
+ }
865
+ for (const token of workspaceEnvTokens(block.vars)) {
866
+ if (!optionKeys.has(token)) {
867
+ ctx.addIssue({
868
+ code: z2.ZodIssueCode.custom,
869
+ path: ["feature", "workspaceEnv", i, "vars"],
870
+ message: `workspaceEnv template references '\${${token}}', which is not a declared option`
871
+ });
872
+ }
873
+ }
874
+ });
836
875
  if (data.presets) {
837
876
  if (data.category !== "feature") {
838
877
  ctx.addIssue({
@@ -2845,7 +2884,8 @@ function resolveFeatures(opts) {
2845
2884
  devcontainerKey: entry2.feature,
2846
2885
  options,
2847
2886
  persistentHomePaths: [],
2848
- persistentHomeFiles: []
2887
+ persistentHomeFiles: [],
2888
+ workspaceEnv: []
2849
2889
  });
2850
2890
  }
2851
2891
  const aptPackages = [
@@ -2859,7 +2899,8 @@ function resolveFeatures(opts) {
2859
2899
  devcontainerKey: "ghcr.io/devcontainers-contrib/features/apt-packages:1",
2860
2900
  options: { packages: aptPackages.join(",") },
2861
2901
  persistentHomePaths: [],
2862
- persistentHomeFiles: []
2902
+ persistentHomeFiles: [],
2903
+ workspaceEnv: []
2863
2904
  });
2864
2905
  }
2865
2906
  if (opts.features) {
@@ -2869,6 +2910,7 @@ function resolveFeatures(opts) {
2869
2910
  const name = match.name;
2870
2911
  const descriptor = featureDescriptor(name);
2871
2912
  const { paths, files } = descriptorPersistentHome(descriptor);
2913
+ const workspaceEnv = descriptor?.feature?.workspaceEnv ?? [];
2872
2914
  const sourceRoot = featuresSourceRoot();
2873
2915
  const localSourceDir = sourceRoot ? path10.join(sourceRoot, name) : null;
2874
2916
  if (descriptor && localSourceDir && existsSync7(localSourceDir)) {
@@ -2879,7 +2921,8 @@ function resolveFeatures(opts) {
2879
2921
  localName: name,
2880
2922
  generatedManifest: descriptorToFeatureManifest(descriptor),
2881
2923
  persistentHomePaths: paths,
2882
- persistentHomeFiles: files
2924
+ persistentHomeFiles: files,
2925
+ workspaceEnv
2883
2926
  });
2884
2927
  continue;
2885
2928
  }
@@ -2887,7 +2930,8 @@ function resolveFeatures(opts) {
2887
2930
  devcontainerKey: rawRef,
2888
2931
  options,
2889
2932
  persistentHomePaths: paths,
2890
- persistentHomeFiles: files
2933
+ persistentHomeFiles: files,
2934
+ workspaceEnv
2891
2935
  });
2892
2936
  continue;
2893
2937
  }
@@ -2895,7 +2939,8 @@ function resolveFeatures(opts) {
2895
2939
  devcontainerKey: rawRef,
2896
2940
  options,
2897
2941
  persistentHomePaths: [],
2898
- persistentHomeFiles: []
2942
+ persistentHomeFiles: [],
2943
+ workspaceEnv: []
2899
2944
  });
2900
2945
  }
2901
2946
  }
@@ -2915,6 +2960,33 @@ function descriptorPersistentHome(descriptor) {
2915
2960
  files: filterFileEntries(descriptor?.feature?.persistentHomeFiles)
2916
2961
  };
2917
2962
  }
2963
+ function isOptionEnabled(value) {
2964
+ if (typeof value === "boolean") return value;
2965
+ if (typeof value === "number") return value !== 0;
2966
+ if (typeof value === "string") return value !== "" && value !== "false";
2967
+ return false;
2968
+ }
2969
+ function featureWorkspaceEnv(features) {
2970
+ const env = {};
2971
+ for (const f of features) {
2972
+ for (const block of f.workspaceEnv) {
2973
+ if (block.whenOption !== void 0 && !isOptionEnabled(f.options[block.whenOption])) {
2974
+ continue;
2975
+ }
2976
+ for (const [name, template] of Object.entries(block.vars)) {
2977
+ const value = template.replace(
2978
+ /\$\{([A-Za-z0-9_]+)\}/g,
2979
+ (_, token) => {
2980
+ const v = f.options[token];
2981
+ return v === void 0 || v === null ? "" : String(v);
2982
+ }
2983
+ );
2984
+ if (value !== "") env[name] = value;
2985
+ }
2986
+ }
2987
+ }
2988
+ return env;
2989
+ }
2918
2990
  function filterSubpaths(raw) {
2919
2991
  if (!Array.isArray(raw)) return [];
2920
2992
  return raw.filter(
@@ -3034,6 +3106,8 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3034
3106
  }
3035
3107
  } : void 0;
3036
3108
  const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
3109
+ const workspaceEnv = featureWorkspaceEnv(resolvedFeatures);
3110
+ const containerEnvField = Object.keys(workspaceEnv).length > 0 ? { containerEnv: workspaceEnv } : void 0;
3037
3111
  if (needsCompose(opts)) {
3038
3112
  return {
3039
3113
  name: opts.name,
@@ -3074,6 +3148,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3074
3148
  forwardPorts: ports,
3075
3149
  postCreateCommand: ".devcontainer/post-create.sh",
3076
3150
  ...sshPostStart,
3151
+ ...containerEnvField ?? {},
3077
3152
  ...featuresField ?? {},
3078
3153
  ...customizationsField ?? {}
3079
3154
  };
@@ -3123,7 +3198,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3123
3198
  for (const v of ideVolumes) {
3124
3199
  lines.push(` - ${v.volume}:${v.target}`);
3125
3200
  }
3126
- const wsEnv = serviceConnectionEnv(opts.services);
3201
+ const wsEnv = {
3202
+ ...serviceConnectionEnv(opts.services),
3203
+ ...featureWorkspaceEnv(resolvedFeatures)
3204
+ };
3127
3205
  const wsEnvKeys = Object.keys(wsEnv);
3128
3206
  if (wsEnvKeys.length > 0) {
3129
3207
  lines.push(" environment:");
@@ -3812,26 +3890,39 @@ function addInstallUrlToDoc(doc, url) {
3812
3890
  seq.add(url);
3813
3891
  return true;
3814
3892
  }
3815
- function addFeatureToDoc(doc, ref, options = {}, displayName) {
3893
+ function addFeatureToDoc(doc, ref, options = {}, opts = {}) {
3816
3894
  const seq = ensureSeq(doc, "features");
3817
- const label = displayName ?? ref;
3895
+ const label = opts.displayName ?? ref;
3818
3896
  const summary = loadFeatureManifestSummary(ref);
3819
- const hints = featureOptionHints(summary, ref, Object.keys(options));
3820
- const mergedOptions = { ...options };
3821
- for (const h of hints) mergedOptions[h.key] = h.placeholder;
3897
+ const withHints = (o) => {
3898
+ const out = { ...o };
3899
+ for (const h of featureOptionHints(summary, ref, Object.keys(out))) {
3900
+ out[h.key] = h.placeholder;
3901
+ }
3902
+ return out;
3903
+ };
3822
3904
  for (const item of seq.items) {
3823
3905
  if (!isMap2(item)) continue;
3824
3906
  const itemRef = item.get("ref");
3825
3907
  if (itemRef !== ref) continue;
3826
3908
  const itemJs = item.toJS(doc);
3827
3909
  const existingJs = itemJs.options ?? {};
3828
- if (JSON.stringify(existingJs) === JSON.stringify(mergedOptions)) {
3910
+ if (opts.isPreset) {
3911
+ const merged = withHints(mergeFeatureOptions(existingJs, options));
3912
+ if (JSON.stringify(existingJs) === JSON.stringify(merged)) {
3913
+ return false;
3914
+ }
3915
+ item.set("options", merged);
3916
+ return true;
3917
+ }
3918
+ if (JSON.stringify(existingJs) === JSON.stringify(withHints(options))) {
3829
3919
  return false;
3830
3920
  }
3831
3921
  throw new Error(
3832
3922
  `Feature ${label} is already configured with different options. Remove it first (\`monoceros remove-feature ${label}\`) before re-adding.`
3833
3923
  );
3834
3924
  }
3925
+ const mergedOptions = withHints(options);
3835
3926
  const entry2 = new YAMLMap2();
3836
3927
  entry2.set("ref", ref);
3837
3928
  if (Object.keys(mergedOptions).length > 0) {
@@ -3993,6 +4084,7 @@ var init_yml = __esm({
3993
4084
  "use strict";
3994
4085
  init_scaffold();
3995
4086
  init_manifest();
4087
+ init_components();
3996
4088
  init_feature_doc();
3997
4089
  init_env_file();
3998
4090
  GIT_USER_HEADER_COMMENT = [
@@ -4340,7 +4432,10 @@ async function runAddFeature(input) {
4340
4432
  };
4341
4433
  const result = await mutate(
4342
4434
  input,
4343
- (doc) => addFeatureToDoc(doc, resolved.ref, merged, raw)
4435
+ (doc) => addFeatureToDoc(doc, resolved.ref, merged, {
4436
+ isPreset: resolved.isPreset,
4437
+ displayName: raw
4438
+ })
4344
4439
  );
4345
4440
  if (result.status === "updated") {
4346
4441
  const summary = loadFeatureManifestSummary(resolved.ref);
@@ -4368,7 +4463,7 @@ async function runAddFeature(input) {
4368
4463
  }
4369
4464
  async function resolveFeatureRefOrShortname(input) {
4370
4465
  if (REGEX.featureRef.test(input)) {
4371
- return { ref: input, defaultOptions: {} };
4466
+ return { ref: input, defaultOptions: {}, isPreset: false };
4372
4467
  }
4373
4468
  const catalog = await loadComponentCatalog();
4374
4469
  const component = catalog.get(input);
@@ -4400,7 +4495,8 @@ async function resolveFeatureRefOrShortname(input) {
4400
4495
  const [first] = features;
4401
4496
  return {
4402
4497
  ref: first.ref,
4403
- defaultOptions: { ...first.options ?? {} }
4498
+ defaultOptions: { ...first.options ?? {} },
4499
+ isPreset: input.includes("/")
4404
4500
  };
4405
4501
  }
4406
4502
  function runRemoveLanguage(input) {
@@ -4683,7 +4779,7 @@ var init_add_feature = __esm({
4683
4779
  meta: {
4684
4780
  name: "add-feature",
4685
4781
  group: "edit",
4686
- description: "Add a devcontainer feature by ref to the container config. Options follow `--` as `key=value` pairs. Idempotent (same ref + same options is a no-op). Adding the same ref with different options is an error."
4782
+ description: "Add a devcontainer feature by ref to the container config. Options follow `--` as `key=value` pairs. Adding a sub-tool selector (e.g. `atlassian/forge`) to an already-present feature merges it in additively, keeping the other sub-tools on. A plain feature already present with different options is an error (remove + re-add to change it)."
4687
4783
  },
4688
4784
  args: {
4689
4785
  name: {
@@ -4846,7 +4942,7 @@ var init_add_repo = __esm({
4846
4942
  },
4847
4943
  provider: {
4848
4944
  type: "string",
4849
- description: "Git provider for credential-helper guidance: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right CLI (gh / glab / Atlassian token) on missing credentials."
4945
+ description: "Git provider for credential-helper guidance: github | gitlab | bitbucket | gitea. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right auth (gh / glab / a provider token) on missing credentials."
4850
4946
  },
4851
4947
  yes: {
4852
4948
  type: "boolean",
@@ -5400,7 +5496,9 @@ async function setupWindowsBridge(name, hostAlias, privateKey, deps, logger) {
5400
5496
  }
5401
5497
  const monoDir = path13.join(profile.homeWsl, ".ssh", "monoceros");
5402
5498
  await fs10.mkdir(monoDir, { recursive: true });
5403
- await fs10.copyFile(privateKey, path13.join(monoDir, name));
5499
+ const keyDst = path13.join(monoDir, name);
5500
+ await fs10.rm(keyDst, { force: true });
5501
+ await fs10.copyFile(privateKey, keyDst);
5404
5502
  const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
5405
5503
  await upsertMarkedBlock(
5406
5504
  path13.join(profile.homeWsl, ".ssh", "config"),
@@ -8082,7 +8180,7 @@ var CLI_VERSION;
8082
8180
  var init_version = __esm({
8083
8181
  "src/version.ts"() {
8084
8182
  "use strict";
8085
- CLI_VERSION = true ? "1.29.1" : "dev";
8183
+ CLI_VERSION = true ? "1.30.0" : "dev";
8086
8184
  }
8087
8185
  });
8088
8186